Skip to content

Beta to main - #708

Merged
remko48 merged 226 commits into
mainfrom
beta
Sep 29, 2026
Merged

remko48 merged 226 commits into
mainfrom
beta

Conversation

@remko48

@remko48 remko48 commented Sep 29, 2026

Copy link
Copy Markdown
Member

No description provided.

github-actions Bot and others added 30 commits September 6, 2026 12:49
…24911

chore(sync): carry main back into beta
…260906124917

chore(sync): carry beta back into development
The staged lockfile still carried seven unresolved merge-conflict markers
from the @conduction/nextcloud-vue 2.25.1 -> 2.36.3 merge. Regenerating it
resolves those and lifts fast-uri 3.1.5 -> 3.1.7, clearing GHSA-5jgf-p345-68v8,
GHSA-f65p-4m7j-42xc, GHSA-fph4-wmhf-6fwf and GHSA-jqff-g426-hqxp (host
confusion and SSRF, high severity).

The three remaining moderate dompurify advisories arrive through
@toast-ui/editor <- @conduction/nextcloud-vue and have no in-range fix. That
chain is dev-only, consumed solely by scripts/build-icons.js, and never
reaches the bundle.
hydra-gates v1.10.0 -> v1.16.0
nc-vue      2.27.2 -> 2.39.0

Lock-only: both packages are already declared with caret ranges that
permit these versions, so nothing about what this app ACCEPTS changes
- only what it currently resolves to. Opened by the weekly fleet
shared-dependency bump, because a lock nobody re-resolves is a pin
nobody chose.

Merging is gated by this repository's own suite, deliberately: taking
hydra-gates v1.8.1 added patchObject() to a published interface, which
is a load-time fatal for any concrete double that implements it without
the method. CI is the only thing that can tell a safe bump from that.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…n the test bootstrap (#565)

The CLI php.ini sets memory_limit=-1, so a runaway test had nothing to stop it: on 2026-09-08 one openregister test recursed inside the DI container and took 19 GB of RAM. The test bootstrap also loaded the workspace's lib/base.php from a source tree that was never installed, which declares OC and builds a half-built OC::$server before throwing, and that state cannot be undone.

- phpunit.xml and phpunit-unit.xml: hard memory_limit of 2G (the loadable part of the unit suite peaks at 69 MB without coverage)
- tests/bootstrap.php: only boot a root whose config/config.php declares installed => true, otherwise say so on STDERR and run in pure-unit mode; if base.php still throws, stop with exit 1 instead of continuing half-booted
- composer.json: psalm gets --memory-limit=2G and each phpmd call runs under php -d memory_limit=2G

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
The fleet sniff NoServiceLocator flags every \OCP\Server::get() outside
lib/AppInfo, lib/AppHost, lib/Migration, lib/Repair and lib/Resources,
because outside a booted Nextcloud that call autowires services from
scratch and can recurse through a constructor cycle until memory runs out.

Thematiq has exactly one such site, and it cannot be injected. The server
constructs NLDesignEMailTemplate itself, from Mailer::makeTemplate(), with
a fixed argument list, so the class has no constructor of its own to take
a container. The lookup stays where it is and now says why, so the sniff
counts it as resolved rather than as an unexamined lookup.

No behaviour changes.

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
… suite (#569)

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
Make "this token set looks like its brand" a mechanical statement. A new
TokenSetVocabularyAuditService checks every shipped set against three
rules: it must declare the 26 required --nldesign-* semantic tokens the
design system reads, it must not declare --nldesign-* names no stylesheet
consumes, and its --nldesign-color-primary must agree with the manifest's
theming.primary_color. Sets whose design system reads no --nldesign-*
vocabulary (none, summer-breeze) are reported as not auditable.

Measured baseline: 48 shipped sets, 5 complete, 2 not auditable, 41
incomplete. The 41 are recorded in a shrink-only allow-list fixture;
TokenSetVocabularyTest fails both on an unlisted incomplete set and on a
listed set that has started passing, so the list can only shrink. No token
set file changes in this commit.

- scripts/audit-token-sets.mjs mirrors the three rules in dependency-free
  Node (npm run audit:token-sets, --verbose, --json, --check) with a drift
  guard against the PHP required-token list.
- TokenSetService takes the audit service as a constructor dependency and
  appends its verdict to the existing warnings channel as kind 'incomplete';
  the six tests constructing the service directly are updated.
- The admin page gains an "Incomplete set" badge next to the design-system
  badge, a matching banner in the apply dialog, and the same badge in the
  custom-set list. Strings added to en and nl.
- openspec/changes/token-set-vocabulary-audit records the proposal, design,
  tasks and the token-sets spec delta.
Three overrides in Thematiq's own CSS fought Nextcloud's layout, so every
themed page differed from stock in geometry, not only in tokens.

- element-overrides.css set `position: relative !important` on #header,
  which put Nextcloud's out-of-flow header back into normal flow. #content
  is `position: fixed` with `top: auto`, so its offset resolved against a
  static position that now started below the 50px flowed header, and its
  own `margin-top: var(--header-height)` stacked on top: every themed page
  sat about 56px too low with the page background showing as a band between
  header and container (contentTop 106.5px themed vs 50px stock). The rule
  is removed; `overflow: visible` stays so the ribbon may hang below.
- element-overrides.css gave #app-navigation `margin-right: 30px !important`
  and rounded the navigation and content panels individually. #content is a
  flex container with no background that already clips both panels into one
  rounded rectangle, so the margin showed --color-background-plain as a
  blue strip between the panels and the per-panel radius cut notches at the
  seam. Margin and per-panel radius are removed; the container radius stays
  themed once through --body-container-radius in overrides.css.
- admin.css: the apply and theming-sync dialogs scrolled sideways when a
  token table was wider than 700px, so the sticky action bar drifted away
  from the rows and cells appeared beside and below the buttons. The dialog
  now hides x-overflow, the tables scroll inside their own container, and
  the action bar bleeds into the dialog padding so its background covers
  the full strip beneath the buttons.
css/custom-css.css is admin-authored runtime data written on demand by
CustomCssService::write(), never app source. CssInjectionService only emits
the stylesheet when the freeform-CSS feature is enabled and the file has
content, so its absence is the correct default state, and a committed copy
would restyle every instance that enables the feature. Ignore it next to
custom-overrides.css.
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.62.1 to 1.63.0.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](microsoft/playwright@v1.62.1...v1.63.0)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.63.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [stylelint](https://github.com/stylelint/stylelint) from 17.14.1 to 17.15.0.
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](stylelint/stylelint@17.14.1...17.15.0)

---
updated-dependencies:
- dependency-name: stylelint
  dependency-version: 17.15.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
)

Bumps [conduction/hydra-gates](https://github.com/ConductionNL/.github) from 1.16.0 to 1.16.1.
- [Release notes](https://github.com/ConductionNL/.github/releases)
- [Commits](ConductionNL/.github@v1.16.0...v1.16.1)

---
updated-dependencies:
- dependency-name: conduction/hydra-gates
  dependency-version: 1.16.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [phpstan/phpstan](https://github.com/phpstan/phpstan-phar-composer-source) from 2.2.10 to 2.2.13.
- [Commits](https://github.com/phpstan/phpstan-phar-composer-source/commits)

---
updated-dependencies:
- dependency-name: phpstan/phpstan
  dependency-version: 2.2.13
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…#579)

`@vitest/coverage-v8` and `@vitest/ui` both declare `peer vitest` as an exact
version, not a caret, so all three can only ever move as a set. Dependabot opened
them as #573, #575 and #577, and each fails to resolve while the other two sit
at 4.

`vite` is declared explicitly because vitest 5 resolves it from the project where
4 did not; 8.2.2 was already in the lock transitively, so nothing new is pulled in.

    npm run test:unit      -> 9 files, 119 tests passed, exit 0
    npm run test:coverage  -> same, 48.93% statements, exit 0

Co-authored-by: Conduction Release Bot <release-bot@conduction.nl>
The account glyphs on the right of the header were painted with the body
text colour and dimmed to 0.8 opacity, which on a saturated header left
Rotterdam's composited glyph at 1.05:1. They now take
--nldesign-color-header-text at full strength, in both the .header-end
(NC 32) and .header-right (NC 30) layouts, and the invert/brightness
filter that flattened the whole header-end subtree - avatar included -
is gone. App-menu icons follow the set through
--nldesign-header-icon-filter, since images cannot be coloured.

The avatar and its user-status badge keep their own shape and colours:
excluded from the brand-radius rule, from the forced white on the
initials plate, and from the fill/stop-color overrides that rendered the
online badge as a black disc. `img` is no longer in the "remove all
rounded corners" list, so avatars stop being rounded squares everywhere.

The opaque white plate behind the account glyphs came from a rule
written for the dropdown panel that addressed the header ITEM instead.
It now addresses .header-menu__wrapper > .header-menu__content, so the
header colour runs edge to edge.

Sets that ship no img/logos/<id>.svg had a 56px hole where a stock
installation shows the Nextcloud logo, and CSS alone cannot recover it:
an !important declaration whose var() chain ends unresolved still wins
and computes to unset. CssInjectionService::injectLogoUrl() now emits
the fallback chain where the webroot is known - an admin-uploaded logo
as it is, otherwise core's logo.svg masked to the set's own header text
colour.

The settings sidebar is returned to Nextcloud's own selection styling,
instead of a column of underlined brand-coloured links with a tinted,
4px-displaced active row.

On Nextcloud 32 the La Suite search pill is withdrawn to an icon-only
trigger: inline-size: auto grew the button but not the .header-menu
around it, so the following controls were drawn on top of the label. The
NC 34 pill is untouched, and the block records what has to be verified
before the pill comes back.

Rotterdam and Zwolle are regenerated from their published token
packages and drop out of the vocabulary allowlist.

The admin preview's app shell now mirrors Nextcloud's real geometry -
inset rounded container, pill navigation entries with icons, a real
sidebar panel - and reads the header tokens rather than painting the bar
with the primary colour.
The apply and theming-sync dialogs were unusable for the thing they
exist for. Every table column was nowrap, so the font stack alone was
wider than the 700px dialog and pushed the New column behind a
horizontal scrollbar; the colour swatches next to each hex value had
lost the rules that gave them a box, so they rendered zero pixels
wide; and the action bar used sticky positioning, which pins to the
scrollport rather than the dialog, so rows kept drawing below the
buttons. The dialogs that carry a table are now flex columns with the
table as the only scrolling child.

On the themed pages, the rule that makes the dashboard panel
transparent was unscoped, so it matched every Vue app's main element
including the Files panel. With #content transparent above it, the
page background showed as a line down the navigation seam, a wedge in
the rounded corner and a frame along two edges. It is now scoped with
the app class #content already carries.

The active app was marked twice: core's own ::before pill, painted for
a transparent header and therefore white-on-white on a light one, plus
a full-width bar these bundles invented. Core's pill now takes a
colour it is legible against and the bar is gone.

--body-container-radius was mapped to the brand's CONTROL radius,
which squared off the whole app shell for a near-square brand; it is
left to Nextcloud, whose top-corner rounding still comes from the
themed --border-radius-large. The Cunningham set stops applying its
flat 4px to the Nextcloud chrome and stops painting every table
header, and its theming.background_color matches the background its
own stylesheet paints.

OpenWOO is a local test theme, not part of the stack, so it no longer
appears in any source comment or changelog entry; the shipped sets
that illustrate the same roles are named instead. The local
presentation mock is gitignored: usable in a working tree, never
committed.
…termediate, not working correctly yet)

Intermediate commit of the theme converter (MAKEOVER-PLAN.md stage 4,
openspec/changes/nlds-theme-converter). It is committed to keep the work
reviewable and to stop it living only in one working tree; the feature is
NOT finished and does not yet behave correctly end to end from the admin
panel. Do not release from this state.

What is in and verified:

- scripts/mapping/nlds-to-nextcloud.json: the ordered mapping table, the
  never-applied policy, the reason codes, and a logo block. Both runtimes
  load this one file; its SHA-256 goes into every converted file.
- TokenSetConverterService (PHP) and js/lib/tokenConverter.js (dual-mode
  Node/browser) plus scripts/convert-nlds-theme.mjs. The PHP and JS output
  is byte-identical, provenance hash included, over six inputs.
- The converter runs ahead of CustomTokenSetValidator in upload(), which
  now also accepts a pasted `content` body and detects the input by content.
- A theme's inline data: logo is decoded out of its token and written as
  img/logos/{set-id}.{ext}; --nldesign-logo-url and theming.logo point at
  the file and the theme's other logo slots become var(--nldesign-logo-url),
  instead of ~40 KB of base64 in a file served on the login page.
- ThemingService::applyImages() now persists the mime type
  ImageManager::updateImage() returns. Without that app value core kept
  serving its own logo while the sync reported success, which is why no
  synced logo had ever appeared.
- CssParserService no longer truncates values at the `;` inside
  url("data:image/svg+xml;base64,…"); that one unclosed quote used to swallow
  the rest of the :root block.
- The admin dropdown offers only `nextcloud` plus admin-imported sets
  (TokenSetService::SELECTABLE_SHIPPED_SETS) until the 39 incomplete
  shipped sets are regenerated; discovery is untouched.

Not done, and why this is not a working feature yet:

- No parity tests (vitest + PHPUnit); parity was checked by hand only.
- No paste textarea and no report rendering in the admin panel.
- DTCG (input B) is refused in the JS runtime; the CLI does not regenerate
  the dark variant on --write.
- The 39 incomplete sets are not regenerated; the allow-list is still 39.
- PHPUnit could not be run on the authoring machine (no vendor/).
The converter extracts a theme's inline logo for every uploaded or
converted set (TokenSetConverterService::extractLogo() and its JS mirror,
driven by the mapping table's logo block), so a script hard-coded to one
package, one version, one token path and one target file only suggested
that logos were an OpenWOO special case. Nothing called it.
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Selecting a token set, uploading one, deleting one, flipping a variant
toggle or starting a preview all ended in a toast asking the admin to
reload the page, and the theming sync reloaded on a hard-coded timer.
Every judgement about how a theme looks therefore cost a page load, which
is the wrong price for the thing this app exists to do.

The set-dependent part of the cascade is now one ordered list owned by
CssInjectionService: the render emits it and the new manifest endpoint
serves the same list to the admin page, so the two can never disagree
about which files make up a set. js/lib/layerSwap.js inserts the new run,
waits for every sheet to fire load, and only then removes the old one, so
the chrome never flashes unstyled. Elements are found by pathname, because
a link emitted through Util::addStyle() cannot carry an attribute of ours.

The core theming sync was unreachable from the apply dialog's confirm and,
once reachable, opened a second modal on every switch. It is now a checked
section of the apply dialog applied on the same confirm, and that dialog
stays open until the new stylesheets are in effect and the sync has
finished, so its disappearance is the signal that the theme is really on
the page rather than that the request was sent.

Selecting the stock set resets core theming through ThemingDefaults::undo()
instead of matching a manifest entry. Stock is the absence of a synced
theme, not a set of values to copy: matching offered a stale colour and,
having no logo of its own, would have kept the previous brand's wordmark.

ThemingService now persists the mime type ImageManager::updateImage()
returns and marks the background as a plain colour. Without the first, a
sync reported success while every page kept serving Nextcloud's own logo.

css/tokens/nextcloud.css carried pre-Nextcloud-28 values for both the
primary colour and the border radii, so the set that exists to be stock
was not, and the apply dialog pinned those values into custom-overrides.css
where !important kept them through every reset. They are read from the
running instance's own defaults.
Merging development brought vite 8 into package.json while this branch's
lockfile still resolved vite 6, so `npm ci` refused before any tool ran:

  npm error `npm ci` can only install packages when your package.json and
  package-lock.json are in sync
  npm error Invalid: lock file's vite@6.4.3 does not satisfy vite@8.2.2

Every npm job in the quality workflow installs first, so one unresolvable
lockfile failed nine checks at once — build, unit tests, all three frontend
checks, both npm license and security legs, eslint and stylelint — none of
which had anything to do with their own subject matter.

legacy-peer-deps goes with it. It was added because `@gouvfr/dsfr-nexus`
declared peers on two packages that were never published; that package is no
longer in the tree, so the flag bought nothing and would only have hidden the
next peer conflict. Verified on npm 11.19: resolution and a clean `npm ci`
both exit 0 without it, and vitest, prettier and stylelint pass on the
resulting tree.
`npm run format` is a blocking check and nine files failed it: the three
stylesheets and three scripts this branch edited, and the three specs it
added. Formatting only — `prettier --write` output, no logic touched, with
the unit suite still at 140 passing afterwards.
phpcs reported four blocking errors, all in new code. Two `@return` array
shapes ran to 347 and 155 characters against a 150 limit; they are now
multi-line shapes, which phpstan and psalm parse identically. Three inline
ternaries — one building the converter's logo asset, two normalising an
empty upload field to null — are now statements. `emitInlineStyle()` gained
an `$id` parameter without a `@param` line for it.

psalm's two errors are `BackgroundService::DEFAULT_COLOR` and
`DEFAULT_BACKGROUND_COLOR`. OCA\Theming is a first-party Nextcloud app,
present at runtime but not a composer dependency, so it is absent during
analysis — the same reason this file already carries a suppression for
ThemingDefaults, and the method now says so.
Three ignores went stale and phpstan failed on the drift rather than on new
findings: the pattern for `ThemingDefaults::set()` was recorded as occurring
once and now occurs three times, and the reset path added `undo()` plus two
`BackgroundService` constants — all four the same "OCA\Theming is not a
composer dependency" shape the baseline already tracked.

Regenerated with `phpstan --generate-baseline`, the mechanism phpstan.neon
names for per-app debt: 24 entries, no errors outside it. Two of those
entries are NOT of that shape and are real debt in the converter —
`runRules()` declares `&$manifest` nullable while never assigning null, and
its `&$report` out-type carries `reason: mixed` where the signature promises
`string|null`. Both are recorded, not fixed; `@param-out` tags are the
documented remedy when that code is finished.
…atures

Seven PHPUnit errors, all the same defect: a class gained a constructor
argument and its test was not moved with it, so the suite aborted before
asserting anything.

CustomTokenSetController dropped DesignTokensMapper and took
TokenSetConverterService at the end; the audit suite still passed the mapper
in sixth position, where IL10N now sits, and PHP rejected the call on type.
Admin took IRequest as a ninth argument and the initial-state suite still
passed eight.
rubenvdlinde and others added 27 commits September 27, 2026 18:07
… first use) (#671)

thematiq uses nextcloud-vue only as a build-time source for the NL icon
packs, so no bundle here carries Dexie before or after. This keeps the lock
in step with the fleet; the icon build output is byte-identical.
…isions for all 64 (#673)

* docs(openspec): environment marker, restore an earlier version, scheduled theme switch (gov-environment-marker, app-rollback-history, app-scheduled-switch)

* docs(openspec): theme gallery, simple brand form (cat-marketplace, aut-colour-only-ui)

* docs(openspec): theme as code, occ set-theme, token reference, document house style, assistant approved mark (gov-config-as-code, aut-git-sync, app-branding-package, int-cli-set-theme, cat-living-token-docs, sur-generated-documents, sur-assistant-logo)

* docs(parity): gap decisions for the OpenSpec pass, 12 rows specified, 18 decided no, owners filled

* docs(openspec): delegated group house style, per-app brand (gov-lock-per-space, sur-per-app-brand)
…ows, decisions complete (#687)

* docs(openspec): authoring-multi-brand-token-source (aut-multi-brand-themes)

* docs(openspec): authoring-dtcg-export (aut-dtcg-colour-objects)

* docs(openspec): authoring-token-value-types (aut-alpha-hex, aut-light-dark-values, aut-motion-tokens)

* docs(openspec): authoring-token-lifecycle (aut-add-token, gov-token-deprecation)

* docs(openspec): authoring-own-markup-preview (aut-component-preview)

* docs(parity): specify the five authoring changes' 8 rows, decisions file complete (65), motion evidence corrected
…le path out of errors (#701)

A save with an unknown token name or a value the writer strips answered
200 with written set to the input size, and the audit entry listed tokens
that never reached the file. It now answers 400 naming those tokens and
writes nothing. A write failure answers a generic message instead of the
exception text, which carried the absolute file path.

Fixes #694
…and keep alpha (#702)

A DTCG colour given as components was scaled straight to 0-255, so an
srgb-linear colour came out too dark (a linear 0.5 grey became #808080
instead of #bcbcbc), and its alpha was never read, so a translucent colour
imported opaque. Linear components now go through the sRGB transfer
function, and an alpha below 1 is kept as the fourth pair of an 8-digit
hex, also on the hex fallback. Opaque colours stay 6-digit.

Fixes #695
…lpha in dark mode (#703)

ContrastService read only #rgb and #rrggbb and dropped the alpha of
rgba(), so an 8-digit hex was 'not a colour' and faint text was measured
as opaque. It now reads #rgba, #rrggbbaa and the rgba() alpha, and the
contrast audit, the shipped-set audit, the compliance report and the dark
palette's brand check measure a translucent background over the page
background and a translucent foreground over that.

DarkPaletteService derived every dark value as an opaque hex and skipped
8-digit hex tokens. The dark channels still come from the opaque colour,
and a light alpha below 1 is kept as #rrggbbaa. The generator version is
now 3 and every css/tokens/dark file is regenerated with it.

Fixes #696
…tor sets (#704)

The editor writes --animation-quick and --animation-slow, while theme.css
timed its buttons, links and inputs with --nldesign-animation-quick, which
the editor never sets. overrides.css only maps the nldesign name onto the
Nextcloud name, so the admin's speed never reached those transitions and
the page ran two speeds. theme.css now reads --animation-quick, which
overrides.css still feeds from the set's --nldesign-animation-quick when no
override is saved.

Fixes #697
… dark user sees one colour (#705)

Every override sat in one :root block with !important. Nextcloud declares
a chosen dark theme's colours on body, which a :root value never reaches,
so a user who picked Dark theme kept core's colour while a user on System
default with a dark OS saw the override. custom-overrides.css now also
carries the two dark scopes of the generated dark stylesheets for every
brand-layer colour override, with the value DarkPaletteService derives
(the light value when it is not a colour literal). Component tokens are
left out: both dark users already agree on them, and a body copy would
outrank the primary-lock layer.

The override import and the config bundle import read the :root block
alone, so the dark copies in an exported file do not overwrite the light
values.

Fixes #698
…ts own

There was one overrides file, loaded whatever the set. A value an admin pinned
while on some other theme stayed on the page after switching back to
"Nextcloud", so the stock set was not stock, and every theme saved off it wore
the other themes' pinned values too.

A set on the none design system — the stock nextcloud set, and every theme saved
off it — now keeps its edits in css/custom-overrides-{set}.css, and only that
file is loaded while it is the set on the page. The shared custom-overrides.css
stays for every set that wears a design system. The overrides endpoints take an
optional tokenSet so the editor reads and writes the file of the set it is
editing, which need not be the instance's active one.

Resetting to stock is now one request: it empties the stock file and the
previous set's, selects the stock set, and undoes what the sync pushed into
Nextcloud theming, so stock is really stock again. The per-set files are
runtime state and are ignored like custom-overrides.css.
Nextcloud 32 mixes its note-card fills from bare triplets
(rgba(var(--color-success-rgb), 0.1)), so an -rgb token has to hold "r, g, b",
not a colour. normaliseColorForPicker() now turns a triplet into #RRGGBB so the
row gets a swatch, and hexToRgbTriplet() writes a picked colour back as the
triplet, so nobody has to work one out by hand. The registry accepts `rgb` as a
token type alongside color and text.
It is the base every other theme sits on and the one a theme saved from the editor starts from, which "(default)" did not say.
Many component tokens redirected a Nextcloud variable their component never
reads, so the row saved, previewed and moved nothing. Each mapping entry now
names the variable the component actually consumes — the secondary button's
--color-primary-element-light pair, the tertiary's --color-main-text and its
pressed --color-primary-element-light, the note-card glyph's --color-*-text,
the invalid field's --color-border-error, NcSelect's own --vs-* variables — or,
where Nextcloud reads no variable at all, paints the property itself.

A paint is the generator's second mechanism: the rule names the property and
paints it from the token, falling back to what Nextcloud paints there, so an
untouched instance renders as stock. It covers the header background, the
table header fill and weight, the note-card and toast text, the login card
edge (a template, "1px solid {token}", because the guest box draws no border),
the avatar's online dot, a focused input's border, the input and textarea
placeholders, the unchecked checkbox and the tertiary hover. The dialog aliases
its confirm button the way the login button aliases its own, and a heading
inside the empty state or a settings section keeps its component's colour
instead of the heading reset.

The capture of the globals also runs on the token editor's preview, where an
unsaved edit is declared, so the specimens show the edit and not the saved
value. The error and success buttons get the same background, hover and label
tokens; the select and textarea gain the tokens they lacked; the note cards'
error and success border tokens, which nothing reads, are removed. The error
button's label token is read before error-contrast.css falls back to white, and
nldesign's blanket text rule no longer paints over the secondary button's label.
Nextcloud's Theming panel holds the primary and background colours, whether
the background image was removed, and the uploaded background, logo,
navigation-bar logo and favicon. None of that lives in a token set, so a theme
saved from the editor came back without it: applying the theme later
re-coloured the components and left whatever branding was on before.

Saving overrides, or saving the editor's changes as a new theme, now captures
it: the colours, which of the three background states was on (an image of its
own, the image removed, or Nextcloud's default), and a copy of every uploaded
image under img/logos/ and img/backgrounds/ as {set}-captured-{slot}. Images are
copied rather than referenced because core takes an image only as a file and
the slot it came from is overwritten by the next theme that brings one. The
block is kept per set in one app value, shipped and custom sets alike, and laid
over the set's own theming; the stock set is never captured, because its
branding is Nextcloud's own settings.

The theming sync learns the navigation-bar logo, the favicon and the background
state, so applying the theme restores all of it, and a colour applied on its own
no longer drops a captured background image. Resetting to stock undoes the two
new image slots as well. Deleting a custom set forgets its branding and its
copied images, which are runtime state and ignored.
… is set

Saving no longer loses what was saved. The overrides endpoint replaces the
whole file, but the editor sent only the tokens that differed from their
resolved value, and after a reload a saved value reads back as resolved, so
every save deleted the earlier ones and those components fell back to the
primary colour. The editor now sends the saved values plus the edits; after a
save the unsaved-marker dots clear, and the reset button returns to the saved
value rather than the theme's own. Both saves also ask the server to keep
Nextcloud's branding with the theme, and the apply dialog restores the
navigation-bar logo, favicon and background state along with the rest.

Nextcloud's base tokens (--color-main-text and the other globals) are locked
until the admin ticks "Also edit Nextcloud's base tokens" and confirms a warning
that they reach far beyond one component; reset buttons on locked rows are
disabled too. The "ask before saving" controls now read the same way round as
the dialog's "do not ask again": ticked means stop asking.

The playground's specimens answer the pointer: hover rules exist next to every
frozen .is-hover state, the links are real links, the login card's button is
scoped as the login button, and the text input and textarea show their
placeholders, focus and invalid states. A component's shared tokens (the note
cards' and toasts' corner) lead its panel, with a rule between the variants.

The full view of each tab shows that tab: the app mock's buttons, hover
included, are painted from their own tokens, the dialog carries every button,
the note cards and a badge on Buttons & Status and the text styles on
Typography, the content area gets breadcrumbs, a table, form controls and an
avatar status dot, and the login preview uses the login card, field, button,
logo and slogan tokens. The e2e specs unlock the base tokens before editing
--color-primary and clear a field, not reset it, to drop an override.
The coverage ratchet on the changed PHP files dropped by 1.39%: the per-set overrides file, the branding capture on both saves, the captured theming overlay and the new background and image paths of the theming sync had no tests. Each now has one.
capture(), all() and forget() carry the @SPEC reference to the theming metadata requirement they implement, as the rest of the service layer does.
The app mock's table now has text headers, so each th says which cells it heads with scope="col".
… mini table headers and capitalise an inline comment
feat(editor): make every component token reach Nextcloud and keep what a theme saves
Release: merge development into beta
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/thematiq @ b9ed50e

Check PHP Vue Security License Tests
lint ✅
phpcs ✅
phpmd ✅
psalm ✅
phpstan ✅
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-manifest ✅
test-l10n ✅
format ✅
composer ✅ ✅ 104/104
npm ✅ ✅ 2/2
app:check-code ⏭️
info.xml ✅
REUSE ✅
lockfile sync ✅
PHPUnit ✅
Newman ✅
Playwright ❌
Hydra gates ❌

Quality workflow — 2026-09-29 08:13 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/thematiq @ 51dc37d

Check PHP Vue Security License Tests
lint ✅
phpcs ✅
phpmd ✅
psalm ✅
phpstan ✅
phpmetrics ✅
eslint ✅
stylelint ✅
build ✅
check-manifest ✅
test-l10n ✅
format ✅
composer ✅ ✅ 107/107
npm ✅ ✅ 2/2
app:check-code ⏭️
info.xml ✅
REUSE ✅
lockfile sync ✅
PHPUnit ✅
Newman ✅
Playwright ❌
Hydra gates ✅

Quality workflow — 2026-09-29 08:18 UTC

Download the full PDF report from the workflow artifacts.

@remko48
remko48 merged commit 556dfb5 into main Sep 29, 2026
50 of 52 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants