13023 Commits

Author SHA1 Message Date
Andrey Antukh
affb6aec84 🎉 Add telemetry anonymous event collection
Rewrite the audit logging subsystem to support three operating modes and
add anonymous telemetry event collection:

Modes:
- A (audit-log only): events persisted with full context
- B (audit-log + telemetry): same as A, plus events are collected for
  telemetry shipping
- C (telemetry-only): events stored anonymously with PII stripped,
  telemetry flag active, audit-log flag inactive

Audit system refactoring (app.loggers.audit):
- Replace qualified map keys (::audit/name etc.) with plain keywords
- Rename submit! -> submit, insert! -> insert, prepare-event ->
  prepare-rpc-event
- Add submit* as a lower-level public API
- Add process-event dispatch function that handles all three modes and
  webhooks in a single tx-run!
- Add :id to event schema (auto-generated if omitted)
- Add filter-telemetry-props: anonymises event props per event type.
  Keeps UUID/boolean/number values; for login/identify events preserves
  lang, auth-backend, email-domain; for navigate events preserves route,
  file-id, team-id, page-id; instance-start trigger passes through.
- Add filter-telemetry-context: retains only safe context keys.
  Backend: version, initiator, client-version, client-user-agent.
  Frontend: browser, os, locale, screen metrics, event-origin.
- Timestamps truncated to day precision via ct/truncate for telemetry
  storage
- PII stripped: props emptied, ip-addr zeroed, session-linking and
  access-token fields removed from context

Config (app.config):
- Derive :enable-telemetry flag from telemetry-enabled config option

Email utilities (app.email):
- Add email/clean and email/get-domain helper functions for domain
  extraction from email addresses

Setup (app.setup):
- Emit instance-start trigger event at system startup
- Simplify handle-instance-id (remove read-only check)

RPC layer (app.rpc):
- wrap-audit now activates when :telemetry flag is set
- Add :request-id to RPC params context for event correlation

RPC commands (management, teams_invitations, verify_token, OIDC auth,
webhooks): migrate all audit call sites to use the new plain-key API

SREPL (app.srepl.main):
- Migrate all audit/insert! calls to audit/insert with plain keys

Telemetry task (app.tasks.telemetry):
- Restructure legacy report into make-legacy-request; distinguish
  payload type as :telemetry-legacy-report
- Add collect-and-send-audit-events: loop fetching up to 10,000 rows
  per iteration, encodes and sends each page, deletes on success,
  stops immediately on failure for retry
- Add send-event-batch: POSTs fressian+zstd batch (base64 via
  blob/encode-str) to the telemetry endpoint with instance-id per event
- Add gc-telemetry-events: enforces 100,000-row safety cap by dropping
  oldest rows first
- Add delete-sent-events: deletes successfully shipped rows by id

Blob utilities (app.util.blob):
- Add encode-str/decode-str: combine fressian+zstd encoding with URL-
  safe base64 for JSON-safe string transport

Database:
- Add migration 0145: index on audit_log (source, created_at ASC) for
  efficient telemetry batch collection queries

Frontend:
- Always initialize event system regardless of :audit-log flag
- Defer auth events (signin identify) to after profile is set
- Refactor event subsystem for telemetry support

Tests (21 test vars, 94 assertions in tasks-telemetry-test):
- Cover all code paths: disabled/enabled telemetry, no-events no-op,
  happy-path batch send and delete, failure retention, payload anonymity,
  context stripping, timestamp day precision, batch encoding round-trip,
  multi-page iteration, GC cap enforcement, partial failure handling
- blob encode-str/decode-str round-trip tests (14 test vars)
- RPC audit integration tests (5 test vars)

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-10 12:16:50 +02:00
Andrey Antukh
10a23a6869 Merge remote-tracking branch 'origin/main' into staging 2026-05-10 09:16:41 +02:00
Eva Marco
cccd7bc6de
🐛 Fix pixel grid color row (#9360) 2026-05-08 11:06:56 +02:00
Andrey Antukh
279231240d
🐛 Harden outbound HTTP requests against SSRF and restrict assets handlers (#9390)
* ⬆️ Update root deps

* 🐛 Harden outbound HTTP requests against SSRF and restrict unauthenticated asset access

- Add app.util.ssrf URL/host validator that resolves hostnames and blocks
  loopback, link-local, site-local, cloud metadata, and operator-supplied CIDRs
- Add app.media.sanitize image EOF truncator that strips trailing data after
  PNG IEND, JPEG EOI, GIF trailer, and WebP RIFF markers
- Disable HTTP client auto-redirect; add req-with-redirects! helper that
  revalidates every redirect hop against the SSRF blocklist
- Wire SSRF validation and EOF sanitization into media/download-image
- Validate webhook URLs and OIDC profile picture URLs against SSRF
- Restrict /assets/by-id to require authentication for non-public buckets
  (profile) while keeping public access for file-media-object,
  file-object-thumbnail, team-font-variant, and file-data-fragment
- Add config knobs: ssrf-protection-enabled, ssrf-allowed-hosts,
  ssrf-extra-blocked-cidrs

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-08 09:18:22 +02:00
Eva Marco
c5f2ffab69
🐛 Fix internal error when applying not valid value to margin input (#9311) 2026-05-07 15:24:25 +02:00
Alejandro Alonso
03487f90e5 🐛 Fix double-clicking a text element selected via Ctrl+click in nested layouts jumps to parent instead of entering edit mode 2026-05-07 09:57:15 +02:00
Dexterity
db77780227 🐛 Fix MCP "active in another tab" notification not clearing (#9321) 2026-05-06 17:42:34 +02:00
Dexterity
ae7c7a7972 🐛 Fix swapped analytics event names on MCP tab-switch dialog (#9322) 2026-05-06 17:41:16 +02:00
Elena Torro
97511ba6e5 Translation-aware modifier propagation and lazy parent walks 2026-05-06 15:10:26 +02:00
Elena Torro
9230091492 Coalesce live drag preview state and reduce sidebar churn 2026-05-06 15:10:26 +02:00
Andrey Antukh
14a0660352 Merge remote-tracking branch 'origin/main' into staging 2026-05-06 15:06:59 +02:00
Andrey Antukh
9e681260cc
🐛 Fix incorrect invitation token handling on register process (#9380)
* 🐛 Fix incorrect invitation token handling on register process

- Reject prepare-register-profile when an active profile already
  exists for the requested email.
- Stop embedding an existing profile's :profile-id into the
  prepared-register JWE. Profile resolution in register-profile is
  now done exclusively by email lookup, never by a JWE claim.
- Add created? guard to the invitation-success branch in
  register-profile, so existing profiles (active or not) cannot
  reach session creation via anonymous registration.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* ♻️ Restructure invitation handling inside register-profile

Move the invitation-success branch into the created? sub-cond so it
sits alongside the other post-creation branches, making the control
flow consistent.

- Active new profile + matching invitation: mint session and return
  :invitation-token (frontend redirects to :auth-verify-token).
- Not-yet-active new profile + matching invitation: embed the
  invitation token inside the verify-email JWE and send the
  verification email. When the user clicks the link, they get
  logged in and the frontend completes the team-invitation flow.
- Extend send-email-verification! with an optional invitation-token
  parameter propagated into the verify-email JWE claims.
- Update the frontend verify-email handler to navigate to
  :auth-verify-token when the response carries :invitation-token.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

* 🐛 Handle email-already-exists error on registration form

Add a specific handler for the [:validation :email-already-exists] error
code in the registration form's on-error callback. The backend raises
this error when an active profile already exists for the requested email,
but the frontend was falling through to the generic error message.

Now it shows the existing "Email already used" i18n message instead of
the generic "Something wrong has happened" toast.

* 🐛 Reset submitted state on registration form error

The on-error handler in the registration form was not resetting the
submitted? state, causing the submit button to remain disabled after
any error. The completion callback in rx/subs! only fires on success,
not on error.

Add (reset! submitted? false) at the beginning of the on-error handler
so the form becomes submittable again after any error, allowing the user
to fix their input and retry.

---------

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-06 14:20:55 +02:00
Alonso Torres
2fbff4f88e
Buffer update shapes changes on token application 2026-05-06 12:16:35 +02:00
Alejandro Alonso
528d006b8d
Events enhancements
*  Compute moved-subtree data once in find-valid-parent-and-frame-ids
*  Smooth WASM drag preview stream
*  Limit WASM outline modifier application
2026-05-06 12:16:09 +02:00
Andrey Antukh
34cc0e9d56 🔥 Materialize the canary feature flag across the codebase
Remove the :canary flag from the flags definition and make all
features gated behind it always available:

- Enable "download font" option in dashboard fonts context menu
- Enable Tab/Shift+Tab keyboard navigation for renaming shapes
  in layer items
- Enable "duplicate color" option in asset panel when applicable
- Enable "duplicate typography" option in asset panel when applicable
- Enable "copy as image" context menu option for frame shapes

Also remove unused [app.config :as cf] requires from files that
no longer reference it after the materialization.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-05-06 11:19:04 +02:00
Eva Marco
5f40673fde
🐛 Remove drag to change when there is a token applied on numeric-inputs (#9314) 2026-05-06 10:31:39 +02:00
Hosted Weblate
94f8370d98
🌐 Merge branch 'origin/develop' into Weblate. 2026-05-06 08:58:11 +02:00
Eva Marco
ce24fed32b
🐛 Fix incorrect text-edition warning when applying tokens (#9355) 2026-05-06 08:47:30 +02:00
Alejandro Alonso
67bb109331 📎 Fix linting issues 2026-05-05 18:32:25 +02:00
Alejandro Alonso
00c27287bd Merge remote-tracking branch 'origin/staging' into develop 2026-05-05 18:27:09 +02:00
Alejandro Alonso
2e8d188d87 🐛 Fix allow selection while bounding-box cloaking is active 2026-05-05 16:55:28 +02:00
Marina López
41996ed9a5 Add nitrate subscription text 2026-05-05 13:52:01 +02:00
Pablo Alba
3431aee177 🐛 Fix move org dialog must be select 2026-05-05 12:34:40 +02:00
Alejandro Alonso
7d923f8e1d Merge remote-tracking branch 'origin/staging' into develop 2026-05-05 06:52:38 +02:00
Alejandro Alonso
c794e0ed73
🎉 Events enhancements (#9310)
* 🎉 Coalesce viewport pointermove into one PointerEvent

* 🎉 Skip worker hover selection query during transform

* 🎉 Coalesce snap X/Y into one worker query-snap-xy
2026-05-04 17:57:00 +02:00
Dominik Jain
07ad152ae5 🐛 Fix .component() returning outermost component for nested instances
The shape API method .component() used locate-component which walks
to the outermost instance root via get-instance-root. For nested
component instances (e.g. a button inside a card), this incorrectly
returned the outer component (the card) instead of the nearest one
(the button).

Added locate-head-component in utils.cljs which uses get-head-shape
to find the nearest component head, and updated the :component
property in shape.cljs to use it.

Fixes #9183
2026-05-04 16:13:25 +02:00
Clayton
4ce56e96fe
🐛 Fix MCP media uploads and SVG data URI image parsing (#9201)
* 🐛 Fix MCP media uploads and SVG data URI image parsing

Signed-off-by: Clayton <claytonlin1110@gmail.com>

* 🐛 Fix lint

Signed-off-by: Clayton <claytonlin1110@gmail.com>

* 🐛 Fix test

Signed-off-by: Clayton <claytonlin1110@gmail.com>

---------

Signed-off-by: Clayton <claytonlin1110@gmail.com>
2026-05-04 13:33:58 +02:00
Eva Marco
a2bcbe81dd
🎉 Add token numeric inputs for inputs on right sidebar (#9143)
Co-authored-by: Xavier Julian <xavier.julian@kaleidos.net>
2026-05-04 13:02:19 +02:00
Alejandro Alonso
164f0cba7a Merge remote-tracking branch 'origin/staging' into develop 2026-05-04 11:56:07 +02:00
María Valderrama
152967bea6 🐛 Fix sidebar overflow 2026-05-04 11:02:54 +02:00
María Valderrama
f24ad6bee4
Show current plan in Nitrate
*  Show current plan in Nitrate

* 📎 Code Review
2026-05-04 09:29:14 +02:00
alonso.torres
f6bd991968 🐛 Improved e2e tests stability 2026-05-04 09:04:30 +02:00
Jack Storment
8f03b5ed9c
🔥 Remove stray debug log in frame-preview load-ref callback (#9258)
`(.log js/console "load-ref" iframe-dom)` was left in the iframe
ref callback of `frame-preview`. Mirrors the defect PR #9243
removed from `color-row*` — fires on every ref invocation and
pollutes the browser console.

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-30 16:21:40 +02:00
FairyPiggyDev
76c1b9afab
♻️ Migrate navigation-bullets to modern component syntax (#9265)
Step toward issue #9260 (incremental migration of legacy UI
components to the modern `*`-suffixed syntax, removing the per-render
JS-to-Clojure props conversion overhead).

Component
---------

`app.main.ui.releases.common/navigation-bullets` is a small (7-line)
self-contained presentational component used by every release-notes
modal to render the slide-progress dots. It already used standard
keyword destructuring (`[{:keys [slide navigate total]}]`), had no
`?`-suffixed props, no `unchecked-get`, no `obj/merge!`, no
`::mf/wrap-props false`, and (importantly) no `::mf/register`, so it
satisfies the migration pre-flight checks unchanged.

Changes
-------

- `releases/common.cljs` — definition renamed to `navigation-bullets*`.
  Body, props and metadata are otherwise unchanged.

- `releases/v1_4.cljs` … `v2_15.cljs` (29 files) — every existing call
  site `[:& c/navigation-bullets {…}]` becomes
  `[:> c/navigation-bullets* {…}]`. The `:slide`, `:navigate`, `:total`
  props are passed exactly as before. The `:as c` alias of the require
  is unchanged, so no require edits are needed.

No props were renamed (none ended in `?`); no helpers had to be
swapped for `mf/spread-props` / `mf/props` (callers pass plain literal
maps); no metadata had to be removed (none of the legacy options were
in use).

Github #9260

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
2026-04-30 15:50:38 +02:00
Juan Flores
4902037c7d
Add HEX, HSB, and HSL support in the third color tab (#9134)
*  Add HEX, HSB, and HSL support in the third color tab

Relabel the existing HSVA tab to HSBA (the math was already HSB) and add
an inline HSB ↔ HSL model toggle inside the tab, matching Figma's color
panel. Sliders, gradients, and labels update dynamically per mode; HSL
values roundtrip through RGB/HSV so no color-storage changes are needed.
Model choice persists across sessions.

* 💄 Fix lint errors

Signed-off-by: juan-flores077 <toptalent399@gmail.com>

* 🐛 Fix Plugin API token application for JS array of strings (#9166)

* 🐛 Fix Plugin API token application for JS array of strings

Plugin code calling `shape.applyToken(token, ["fill"])` or
`token.applyToShapes([rect], ["fill"])` from JavaScript supplies a JS
array of strings. The plugin proxies expected a Clojure set of
keywords, and two coupled defects made the calls silently no-op (or,
with `throwValidationErrors` enabled, throw "check error"):

1. `token-attr-plugin->token-attr` only consulted its alias map when
   the input was already a keyword. String inputs like "fill" fell
   through to the identity branch, so the downstream
   `cto/token-attr?` predicate (which checks against a set of
   keywords) returned false for every string. Coerce strings to
   keywords first.

2. The `applyToken` / `applyToShapes` / `applyToSelected` schemas
   used plain `[:set ...]`, which has no `:decode/json` transformer
   for JS array → Clojure set coercion. Switch to the registered
   `[::sm/set ...]` (in `app.common.schema`) which provides the
   array → set decoder. After the switch, the standard JSON pipeline
   converts `["fill"]` to `#{"fill"}`, then the inner
   `[:and ::sm/keyword [:fn token-attr?]]` decodes each element to a
   keyword and validates it.

Also extends the docstring on `token-attr-plugin->token-attr` to make
the string-friendly contract explicit, and registers a new
`tokens-test` ns under `frontend/test/frontend_tests/plugins/` with
six `deftest` blocks covering:

- known keywords passing through unchanged
- keyword aliases (`:r1` → `:border-radius-top-left`, etc.)
- string inputs coerced to keywords (regression for #9162)
- `token-attr?` accepting both keyword and string inputs
- `token-attr?` rejecting unknown attrs and nil

Closes #9162

* 🐛 Fix wrong direction in plugin-name alias tests

The added tests in tokens_test.cljs and the new docstring in tokens.cljs
described the alias resolution in the wrong direction. The map is
{:r1 :border-radius-top-left, …} then map-invert'd, so
token-attr-plugin->token-attr maps verbose plugin-side names
(:border-radius-top-left) to canonical internal short names (:r1),
not the other way around. Inputs already in canonical form (:r1, :fill,
"fill", …) pass through unchanged. Flipped the alias-resolution test
expectations and the keyword/string-input cases, refreshed the docstring
and the regression-coverage comment to match.

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>

* 💄 Fix sucess typo in subscription dialog i18n keys (#9204)

Rename subscription.settings.sucess.dialog.{title,footer} to
subscription.settings.success.dialog.{title,footer} in en.po and
update the three callsites in subscription.cljs.

Closes #9203

Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>

* 🐛 Fix HSVA → HSBA test rename and Prettier formatting

Signed-off-by: juan-flores077 <toptalent399@gmail.com>

* 🐛 Fix CI failures and address review feedback for HSB color tab

Signed-off-by: juan-flores077 <toptalent399@gmail.com>

* 💄 Resolve Conflicts

Signed-off-by: juan-flores077 <toptalent399@gmail.com>

---------

Signed-off-by: juan-flores077 <toptalent399@gmail.com>
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: boskodev790 <boskomaljkovic790@outlook.com>
Co-authored-by: Jack Storment <88656337+jack-stormentswe@users.noreply.github.com>
2026-04-30 15:41:04 +02:00
Andrés Moya
9f94566005
💄 Rename i18n keys for tokens errors (#9207) 2026-04-30 15:30:20 +02:00
Statxc
547750e8bf
🐛 Preserve OpenType variant name for custom fonts (#9193) 2026-04-30 15:29:04 +02:00
Elena Torro
27d854ed5b Skip component-sync on pure-translation drag commits 2026-04-30 13:45:45 +02:00
alonso.torres
c14dbba7fd 🐛 Fix z-index for profile menu 2026-04-30 12:59:27 +02:00
Alonso Torres
de9170d96b
🐛 Fix z-index for profile menu (#9257) 2026-04-30 11:27:50 +02:00
Aitor Moreno
acb3997ed7 🐛 Fix text editor v2 min size 2026-04-30 09:21:53 +02:00
FairyPiggyDev
ed021711b6
♻️ Extract make-delete-asset-group-fn helper for assets panel (#9211)
Reviewer follow-up on PR #9151. The "Delete group" handler was
duplicated across the three assets-panel sections (colors,
typographies, components), each carrying the same skeleton — filter
by group path, build an undo-id, run the deletes inside one
transaction, and show the same confirm modal — with only the path
predicate and the per-asset delete event differing.

Add `app.main.ui.workspace.sidebar.assets.common/make-delete-asset-group-fn`
that takes the differing parts as options:

- `:assets`             collection to filter.
- `:on-clear-selection` invoked before the deletes.
- `:delete-events`      `(fn [matching-assets] => seq-of-events)`.
- `:path-filter`        predicate (defaults to `str/starts-with?`),
                        overridden to `cpn/inside-path?` for
                        components so nested group paths match the
                        same way the existing ungroup/combine helpers
                        do.

The factory returns `(fn [path] …)` so each call site stays a
straight `mf/use-fn`. The variant-container dedup in components
(one `delete-shapes` per container, not one per sibling variant)
moves into that section's `:delete-events` fn and is unchanged in
behavior.

Cleanup
-------

The `:as i18n` alias is no longer needed in any of the three section
files (its only use was `i18n/c` for the modal count, which the
helper now handles); reduced to `:refer [tr]`.

Github #9141

Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-04-30 08:48:04 +02:00
TinyClaw
400414776b
🐛 Fix :heigth typo in clipboard frame-same-size? (#9250)
The height comparison in frame-same-size? used the misspelled keyword
:heigth on both sides. (:heigth selrect) returns nil for any selrect,
so (= nil nil) is always true and the function degenerated to a width-only
comparison.

Result: the 'paste next to selected frame' branch in clipboard.cljs fired
whenever pasted-content width matched a target frame's width, even if the
heights differed.

Introduced in #9033 ( Add paste to replace (Cmd+Shift+V)).

Signed-off-by: iot2edge <tylerprice830@gmail.com>
Co-authored-by: iot2edge <tylerprice830@gmail.com>
2026-04-30 08:37:00 +02:00
Andrey Antukh
346614edc3 🐛 Fix SCSS mixin names in v2_15 release modal styles
Rename 5 deprecated mixin calls from camelCase to kebab-case to
match the actual mixin names defined in common-refactor.scss:

- deprecated.flexCenter -> deprecated.flex-center
- deprecated.headlineSmallTypography -> deprecated.headline-small-typography
- deprecated.headlineLargeTypography -> deprecated.headline-large-typography
- deprecated.bodyLargeTypography -> deprecated.body-large-typography
- deprecated.bodyMediumTypography -> deprecated.body-medium-typography

This fixes the "Undefined mixin" SCSS compilation error.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 17:36:20 +00:00
Andrey Antukh
404ebcc63e 💄 Fix SCSS linter errors in v2_15 release modal styles
Replace class selectors with placeholder selectors for @extend
(.modal-overlay-base -> %modal-overlay-base,
 .button-primary -> %button-primary).

Add blank lines after @include mixin calls in 6 rulesets
(.version-tag, .modal-title, .feature-title, .feature-content,
 .feature-list, .next-btn) to satisfy the
declaration-empty-line-before stylelint rule.

Fixes 8 SCSS linter errors in total.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 17:31:59 +00:00
Andrey Antukh
a004219405 Merge remote-tracking branch 'origin/staging' into develop 2026-04-29 19:28:02 +02:00
Andrey Antukh
8b29ca61c6 Merge remote-tracking branch 'origin/main-staging' into staging 2026-04-29 19:23:38 +02:00
Ingrid Pigueron
b5cd4d96ee
🌐 Add translations for: French
Currently translated at 98.8% (2050 of 2074 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-04-29 19:10:01 +02:00
Andrey Antukh
d06b45ec90 🐛 Fix Plugin API token application for JS array of strings
Two coupled defects made shape.applyToken(), token.applyToShapes() and

token.applyToSelected() silently no-op when invoked from JavaScript with

an array of strings (e.g. token.applyToShapes([rect], ["fill"])):

1. token-attr-plugin->token-attr only consulted its alias map when the

   input was already a keyword; string inputs fell through unchanged,

   causing downstream token-attr? to return false.

2. The inner schemas used plain [:set ...] which lacks the :decode/json

   transformer for JS array -> Clojure set coercion. Switching to

   Penpot's custom [::sm/set ...] lets the standard JSON decoder

   pipeline handle the conversion automatically.

This is a backport of commit 1eac3e2be5f973359ad2ec9bac4e80a9d5a9e022

which fixes GitHub #9162.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-04-29 19:09:25 +02:00
Dexterity
f530a0ba26
🔥 Remove stray debug log in color-row component (#9243) 2026-04-29 18:47:27 +02:00