Implement asset-reference resolution plus penpot_add_rect and penpot_add_text #199

Closed
opened 2026-09-14 23:11:26 +00:00 by david · 1 comment
Owner

Summary

Implement library-asset resolution for shape arguments and the two leaf composition primitives, penpot_add_rect and penpot_add_text. Shapes must reference library assets by id (fillColorRefId/fillColorRefFile, typographyRefId/typographyRefFile) so editing one asset restyles every screen that uses it — the whole point of a design system.

Background

Depends on: #198

The agent works in names (fill: "flip7-gold", typography: "h2"); the extension resolves them from the library index built in penpot_list_library and sets the reference fields. The agent never handles raw ids or hex values for linked styling.

Confirmed on 2.17:

  • An asset-linked fill persists as { fillColor, fillOpacity, fillColorRefId, fillColorRefFile } and reads back correctly.
  • A file's own assets use the file's own id as fillColorRefFile; an asset from a linked library file carries that file's id (relevant from the library-import milestone onwards — support the field now, don't hard-code the current file).
  • Text is a real text object whose content is a nested tree: root → paragraph-set → paragraph → spans, where each span carries text, fills and font fields (fontFamily, fontId, fontVariantId, fontSize, fontWeight, fontStyle, lineHeight, letterSpacing, textTransform). The agent should never author this tree by hand — the tool takes a plain string and builds it.
  • Shapes are added with the same add-obj change wrapper as frames, with parentId pointing at the containing frame; the server maintains the parent's shapes array.

Documentation Required

A separate process downloads these into the listed folders before this issue is implemented. Check the folders for the actual reference material before starting.

docs/reference/penpot-api/

docs/reference/pi-coding-agent/ and docs/reference/typebox/

Implementation Details

src/shapes.ts additions (pure, unit-tested)

  • fillFrom({ hex, opacity, refId, refFile }) → a fill record, with fillColorRefId/fillColorRefFile omitted (not null) when there is no reference.
  • textContent({ text, typography, font, fills }) → the full root → paragraph-set → paragraph → spans tree for a single-paragraph string, with each span populated with text, fills and every font field. Build the tree — never accept it from the caller.
  • resolveStyleArg(arg) → parses the dual-form arguments: "#2BA8A2" (literal hex) or { ref: "flip7-gold" } / "colour:flip7-gold"-style selector (library reference). Keep the parser small, documented and covered by tests.

src/tools/rect.tspenpot_add_rect

Parameters: file_id (required), name, x, y, width, height, parent_id (required — the frame it belongs to), page_id (optional), fill (hex or library reference), stroke_color/stroke_width (optional), corner_radius (optional; supports the pill radius = half the shorter side), and child-layout hints where relevant (layout_item_h_sizing / layout_item_v_sizing / layout_item_absolute / layout_item_margin) so rects behave correctly inside an auto-layout frame.

  • Resolve a library-reference fill via the library index; if the name is missing or ambiguous, stage nothing and return the resolution error (which lists available names / candidate ids).
  • Stage an add-obj change and return the shape id in details.

src/tools/text.tspenpot_add_text

Parameters: file_id (required), text (the plain string), name, x, y, width/height (optional; text can size to content), parent_id, page_id, typography (library reference name, optional), font_size/font_weight/line_height/letter_spacing/text_transform/color (used when no typography is named), plus the same child-layout hints.

  • When typography names a library asset, set typographyRefId/typographyRefFile and mirror the typography's font fields into the span (confirmed working on 2.17). When it does not, build the style from the individual parameters.
  • Stage the change; return the shape id.

Both tools: no HTTP write, no update-file; the changeset accumulates and penpot_commit applies.

Acceptance Criteria

  • A library-reference fill produces fillColorRefId and fillColorRefFile (using the asset's owning file id, not a hard-coded current file).
  • A literal hex fill produces no refId/refFile keys at all (not null).
  • An unknown or ambiguous asset name stages nothing and returns the resolution error with available names / candidate ids.
  • Text content is built as a complete root → paragraph-set → paragraph → spans tree with all font fields present per span.
  • Naming a typography sets the typography reference fields and mirrors the asset's font fields into the span.
  • Text without a typography uses the explicit font parameters, with documented defaults for anything omitted.
  • corner_radius supports a pill radius and rejects negatives.
  • Child-layout hints are emitted in the layoutItem* form (fill/fix/auto), never the UI word "hug".
  • Nothing is written before commit; tests assert no update-file call in either tool.
  • Unit tests cover fill construction with and without refs, the text tree shape, typography mirroring, and the resolution/corner-radius error paths.

Test Plan

node --test extensions/penpot/src/shapes.test.ts extensions/penpot/src/tools/rect.test.ts extensions/penpot/src/tools/text.test.ts

Live validation (requires PENPOT_URL/PENPOT_TOKEN, scratch file with library assets from the earlier milestone):

  1. Authorise the scratch file; stage a frame, a rect filled with a library colour reference, and a text using a library typography; commit.
  2. In the Penpot UI: confirm the rect's fill shows as a linked swatch (not a local colour) and the text shows the linked text style.
  3. Edit the referenced colour asset's hex value in the UI, reload the screen, and confirm the rect's fill follows the asset — this is the design-system property the feature exists to provide.
  4. Confirm the text layer is a real text object (selectable, editable text), not an image or path.
  5. Confirm layer names match those passed to the tools.
## Summary Implement library-asset resolution for shape arguments and the two leaf composition primitives, `penpot_add_rect` and `penpot_add_text`. Shapes must reference library assets **by id** (`fillColorRefId`/`fillColorRefFile`, `typographyRefId`/`typographyRefFile`) so editing one asset restyles every screen that uses it — the whole point of a design system. ## Background **Depends on:** #198 The agent works in names (`fill: "flip7-gold"`, `typography: "h2"`); the extension resolves them from the library index built in `penpot_list_library` and sets the reference fields. The agent never handles raw ids or hex values for linked styling. Confirmed on 2.17: - An asset-linked fill persists as `{ fillColor, fillOpacity, fillColorRefId, fillColorRefFile }` and reads back correctly. - A file's own assets use the file's own id as `fillColorRefFile`; an asset from a linked library file carries **that** file's id (relevant from the library-import milestone onwards — support the field now, don't hard-code the current file). - Text is a real text object whose `content` is a nested tree: `root → paragraph-set → paragraph → spans`, where each span carries `text`, `fills` and font fields (`fontFamily`, `fontId`, `fontVariantId`, `fontSize`, `fontWeight`, `fontStyle`, `lineHeight`, `letterSpacing`, `textTransform`). The agent should never author this tree by hand — the tool takes a plain string and builds it. - Shapes are added with the same `add-obj` change wrapper as frames, with `parentId` pointing at the containing frame; the server maintains the parent's `shapes` array. ## Documentation Required A separate process downloads these into the listed folders before this issue is implemented. Check the folders for the actual reference material before starting. **`docs/reference/penpot-api/`** - https://raw.githubusercontent.com/penpot/penpot/2.17.2/common/src/app/common/types/shape.cljc — rect/text shape records and their type-specific fields. - https://raw.githubusercontent.com/penpot/penpot/2.17.2/common/src/app/common/types/fills.cljc — fill record fields, including `fillColorRefId` / `fillColorRefFile`. - https://raw.githubusercontent.com/penpot/penpot/2.17.2/common/src/app/common/types/text.cljc — the `root → paragraph-set → paragraph → span` content tree and its required fields. - https://raw.githubusercontent.com/penpot/penpot/2.17.2/common/src/app/common/types/typography.cljc — the typography asset fields that a text span mirrors, and the `typographyRefId`/`typographyRefFile` reference fields. - `<PENPOT_URL>/api/main/doc/openapi.json` — `update-file`/`add-obj` on this instance. - https://help.penpot.app/user-guide/design-systems/assets/ — how a designer uses swatches and text styles (wording for tool descriptions, and what "linked" means in the UI). **`docs/reference/pi-coding-agent/`** and **`docs/reference/typebox/`** - https://pi.dev/docs/latest/extensions — tool registration and structured `details`. - https://github.com/sinclairzx81/typebox — string/number schema helpers for the text parameters. ## Implementation Details ### `src/shapes.ts` additions (pure, unit-tested) - `fillFrom({ hex, opacity, refId, refFile })` → a fill record, with `fillColorRefId`/`fillColorRefFile` omitted (not null) when there is no reference. - `textContent({ text, typography, font, fills })` → the full `root → paragraph-set → paragraph → spans` tree for a single-paragraph string, with each span populated with `text`, `fills` and every font field. Build the tree — never accept it from the caller. - `resolveStyleArg(arg)` → parses the dual-form arguments: `"#2BA8A2"` (literal hex) or `{ ref: "flip7-gold" }` / `"colour:flip7-gold"`-style selector (library reference). Keep the parser small, documented and covered by tests. ### `src/tools/rect.ts` — `penpot_add_rect` Parameters: `file_id` (required), `name`, `x`, `y`, `width`, `height`, `parent_id` (required — the frame it belongs to), `page_id` (optional), `fill` (hex or library reference), `stroke_color`/`stroke_width` (optional), `corner_radius` (optional; supports the pill radius = half the shorter side), and child-layout hints where relevant (`layout_item_h_sizing` / `layout_item_v_sizing` / `layout_item_absolute` / `layout_item_margin`) so rects behave correctly inside an auto-layout frame. - Resolve a library-reference fill via the library index; if the name is missing or ambiguous, stage nothing and return the resolution error (which lists available names / candidate ids). - Stage an `add-obj` change and return the shape id in `details`. ### `src/tools/text.ts` — `penpot_add_text` Parameters: `file_id` (required), `text` (the plain string), `name`, `x`, `y`, `width`/`height` (optional; text can size to content), `parent_id`, `page_id`, `typography` (library reference name, optional), `font_size`/`font_weight`/`line_height`/`letter_spacing`/`text_transform`/`color` (used when no typography is named), plus the same child-layout hints. - When `typography` names a library asset, set `typographyRefId`/`typographyRefFile` **and** mirror the typography's font fields into the span (confirmed working on 2.17). When it does not, build the style from the individual parameters. - Stage the change; return the shape id. Both tools: no HTTP write, no `update-file`; the changeset accumulates and `penpot_commit` applies. ## Acceptance Criteria - [ ] A library-reference fill produces `fillColorRefId` **and** `fillColorRefFile` (using the asset's owning file id, not a hard-coded current file). - [ ] A literal hex fill produces no `refId`/`refFile` keys at all (not `null`). - [ ] An unknown or ambiguous asset name stages nothing and returns the resolution error with available names / candidate ids. - [ ] Text content is built as a complete `root → paragraph-set → paragraph → spans` tree with all font fields present per span. - [ ] Naming a `typography` sets the typography reference fields and mirrors the asset's font fields into the span. - [ ] Text without a typography uses the explicit font parameters, with documented defaults for anything omitted. - [ ] `corner_radius` supports a pill radius and rejects negatives. - [ ] Child-layout hints are emitted in the `layoutItem*` form (`fill`/`fix`/`auto`), never the UI word "hug". - [ ] Nothing is written before commit; tests assert no `update-file` call in either tool. - [ ] Unit tests cover fill construction with and without refs, the text tree shape, typography mirroring, and the resolution/corner-radius error paths. ## Test Plan ```bash node --test extensions/penpot/src/shapes.test.ts extensions/penpot/src/tools/rect.test.ts extensions/penpot/src/tools/text.test.ts ``` Live validation (requires `PENPOT_URL`/`PENPOT_TOKEN`, scratch file with library assets from the earlier milestone): 1. Authorise the scratch file; stage a frame, a rect filled with a library colour reference, and a text using a library typography; commit. 2. In the Penpot UI: confirm the rect's fill shows as a **linked** swatch (not a local colour) and the text shows the linked text style. 3. Edit the referenced colour asset's hex value in the UI, reload the screen, and confirm the rect's fill follows the asset — this is the design-system property the feature exists to provide. 4. Confirm the text layer is a real text object (selectable, editable text), not an image or path. 5. Confirm layer names match those passed to the tools.
david closed this issue 2026-09-15 03:30:10 +00:00
Author
Owner

pi-loop opened and merged a pull request for this issue: #232

pi-loop opened and merged a pull request for this issue: https://git.excelera.net/david/pi-extensions-and-skills/pulls/232
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
david/pi-extensions-and-skills#199
No description provided.