Skip to content

Release: merge development into beta - #642

Open
github-actions[bot] wants to merge 78 commits into
betafrom
development
Open

github-actions[bot] wants to merge 78 commits into
betafrom
development

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

Automated PR to sync development changes to beta for beta release.

Merging this PR will trigger the beta release workflow.

Reminder: Add a major, minor, or patch label to this PR to control the version bump. Default is patch.

dependabot Bot and others added 30 commits September 18, 2026 07:28
Bumps [@gouvfr/dsfr](https://github.com/GouvernementFR/dsfr) from 1.15.2 to 1.15.3.
- [Release notes](https://github.com/GouvernementFR/dsfr/releases)
- [Changelog](https://github.com/GouvernementFR/dsfr/blob/main/CHANGELOG.md)
- [Commits](GouvernementFR/dsfr@v1.15.2...v1.15.3)

---
updated-dependencies:
- dependency-name: "@gouvfr/dsfr"
  dependency-version: 1.15.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
…260922154440

chore(sync): carry beta back into development
Bumps [twig/twig](https://github.com/twigphp/Twig) from 3.28.0 to 3.29.0.
- [Release notes](https://github.com/twigphp/Twig/releases)
- [Changelog](https://github.com/twigphp/Twig/blob/3.x/CHANGELOG)
- [Commits](twigphp/Twig@v3.28.0...v3.29.0)

---
updated-dependencies:
- dependency-name: twig/twig
  dependency-version: 3.29.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [nextcloud/ocp](https://github.com/nextcloud-deps/ocp) from 34.0.4 to 35.0.0.
- [Commits](nextcloud-deps/ocp@v34.0.4...v35.0.0)

---
updated-dependencies:
- dependency-name: nextcloud/ocp
  dependency-version: 35.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.1/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 5.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [@vitest/coverage-v8](https://github.com/vitest-dev/vitest/tree/HEAD/packages/coverage-v8) from 5.0.0 to 5.0.1.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.1/packages/coverage-v8)

---
updated-dependencies:
- dependency-name: "@vitest/coverage-v8"
  dependency-version: 5.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [@conduction/nextcloud-vue](https://github.com/ConductionNL/nextcloud-vue) from 2.52.0 to 2.55.1.
- [Release notes](https://github.com/ConductionNL/nextcloud-vue/releases)
- [Changelog](https://github.com/ConductionNL/nextcloud-vue/blob/main/CHANGELOG.md)
- [Commits](ConductionNL/nextcloud-vue@v2.52.0...v2.55.1)

---
updated-dependencies:
- dependency-name: "@conduction/nextcloud-vue"
  dependency-version: 2.55.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Bumps [jsdom](https://github.com/jsdom/jsdom) from 30.0.1 to 30.1.0.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Commits](jsdom/jsdom@v30.0.1...v30.1.0)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 30.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
The token editor could only write Nextcloud's globals, and Nextcloud has one
global per concern, not per component. `--color-primary-element` alone paints
the primary button, the selected navigation entry, the sidebar's active tab, a
focused input, the checked checkbox, the progress bar, the dialog's confirm
button and the counter bubble. So the component playground — built precisely so
an admin could style one component while looking at it — could not reach a
single component: every chip moved seven other things with it.

`scripts/mapping/component-tokens.json` makes the mapping data: per component
the selectors it occupies, and per token the global it replaces, its label, its
type and whether the brand primary used to drive it. 123 component tokens across
33 components, following the precedent `scripts/mapping/nlds-to-nextcloud.json`
set, so the editable registry and the stylesheet that applies it read one source
and cannot drift.

`generate-component-scopes.mjs` emits two files from it.
`css/component-scopes.css` re-scopes rather than repaints: inside each
component's subtree it redeclares the Nextcloud variable that component
consumes, pointing it at the component token with the captured
`--thematiq-global-*` behind it, and Nextcloud's own stylesheets keep doing the
painting. The alternative — an `!important` element rule per component, the
house style in theme.css — was rejected: twenty new rule blocks written against
Nextcloud internals, each able to drift on a release, to achieve what
redirecting one variable achieves. `css/primary-lock.css` is the other half,
emitted for the toggle that gives the primary its reach back.

Both are byte-compared against the committed copies by `npm run
test:component-scopes`, so prettier must leave them alone or it would fail that
check on a file nobody edited.
`defaults.css` already declared 104 `--nldesign-component-*` tokens and
theme.css already painted buttons, links, headings and form fields from some of
them, but the two halves never fully met: several rules still read a brand token
directly, so a component token could be set and nothing would move.

Every such rule now reads the component token first with the brand token behind
it, which is what makes a per-component value visible without changing anything
for an instance that has set none. The split by component is decided here rather
than in the generated scope layer: buttons, inputs, selects, textareas and the
modal container get their own rule blocks instead of one shared selector list,
because a shared list cannot give the login button a colour the main button does
not get.

Headings gain three tokens the six per-level tokens now derive from —
`--nldesign-component-heading-{font-family,font-weight,color}` — so the
playground's single `Headings` chip moves h1 through h6 at once, while a token
set that wants one level to differ still overrides that level's own token and
wins, because its declaration replaces the derivation.
`TokenRegistry::getTokens()` reads the mapping and returns the brand globals
plus the 123 component tokens, each carrying `group` — `brand`, or the component
id — and `primary`, which marks a token the brand primary used to drive. The
brand globals stay editable from the four-tab list: that list IS the brand-level
control, and moving one is MEANT to move every component that has not opted out.

`primary_drives_components` is the opt-out, off by default, which changes
nothing visually — with no per-component value stored the component tokens
already resolve to the brand primary. While it is on, CssInjectionService emits
`primary-lock.css` last of all, because it and `custom-overrides.css` both write
`--nldesign-component-*` at `:root` with `!important` and the later one wins;
turning the setting off drops the layer and the stored values take effect again,
since nothing is ever deleted.

The scope layer is emitted after the design-system layers, whose `:root`
declarations it captures, and before the admin's own overrides, which are
allowed to move the brand values those captures resolve to. The setting travels
in the config bundle so an OTAP promotion carries it, and it is validated as a
boolean on import rather than coerced.
The token editor renders the registry, so the component layer arrives in it on
its own; what it did not have was a way to read 123 more rows. Rows now group by
component under their own headings, and the four brand tabs keep listing the
globals exactly as before.

A row the `primary_drives_components` setting has taken ownership of renders
disabled and dimmed rather than hidden: the stored value is still there and
comes back the moment the setting is switched off, and a row that vanished would
read as a value that was thrown away. `cursor: not-allowed` sits on the row
rather than the input, because a disabled input fires no pointer events and the
cursor would never change over the control itself. The row carries the reason as
a title, so the lock explains itself where it is met.

The toggle itself is a checkbox beside the existing theming options, posting to
its own route. Three new strings, l10n catalogues rebuilt.
All 34 chips named a Nextcloud global, so the control a chip rendered did
something other than what its title said: moving `Primary button` moved seven
other components with it, and moving the login button was impossible without
moving the main button too. Each chip now names the component token for the
component it draws.

The stage marks the component it is drawing with `data-thematiq-component`, so
the theme can reach a specimen the way it reaches the real thing. A specimen
carries the component's real class names, which is enough for every component
whose rules are class-scoped; it is not enough for `#header` and the three under
`#body-login`, because a specimen cannot carry an id that belongs to the page it
is drawn on. The login button is the visible case — its specimen is the same
`.button-vue--primary` markup as the primary button's, so without the hook it
was painted by the primary button's token.

The header specimens read their component token ahead of the set's own header
colour. Reading `--nldesign-color-header-background` alone was not enough once
the header had a component token: a token set DECLARES that colour, so the
fallback behind it never fired and the specimen ignored anything set on the
Header bar chip.

`playgroundInventory.spec.js` exempts the brand globals from its coverage rule
and adds the inverse check — that no chip names a global — so the defect this
fixes cannot come back silently.
A component token is worth nothing unless something paints from it, and
existence was never the problem: the login button shipped with a token that
stored a colour, rendered a control and moved nothing, because a thematiq rule
painted the property with `!important` while reading the primary button's token
instead.

The guard accepts the two ways a token can actually reach its component — the
scope layer redirects the Nextcloud variable and a Nextcloud rule paints from
it, or a thematiq rule paints the property and reads a token that chains to the
component's own, directly, through defaults.css, or through an `aliases` entry
in the mapping — and fails the case above, where the rule wins and nothing says
so.
Proposal, design and tasks for the layer, plus the `component-tokens` spec delta
covering the component layer and the `primary_drives_components` toggle.
`table-layout: fixed` with declared percentages bounded the column, not the
content: a nowrap timestamp or a token set name wider than its 15% drew over the
cell beside it, so Timestamp overlapped User and From overlapped To on every
row. The `overflow-x` that was supposed to catch this sat inside a
`max-width: 480px` query, so the settings page never reached it and the table
escaped its container instead.

The table now sizes to its content with a 72em floor and the container scrolls
at every width — the container, not the <table>, because `display: block` on a
table drops its implicit table role in Chrome and Firefox and unassociates every
`th[scope=col]` from its cells, on the panel that exists to be shown to an
accessibility auditor.

The panel is dressed to match: a bordered, rounded scroller, cell padding on
both sides, a tinted header row, banding for rows wider than the viewport,
tabular-nums timestamps, and From and To pinned to a common width so the two
sides of a change line up.
Raises max-version to 35. The playground already draws the Nextcloud 35 header,
so the declaration was the part that had not caught up.
…ources

DarkPaletteService output for eight sets no longer matched what is committed:
zwolle across 71 tokens, cunningham on its background and table header, and a
badge pair plus rotterdam's negative-inverse colour elsewhere. The light sources
are unchanged; these are the derived files catching up.
`Frontend Check (format)` runs `prettier --check` over the whole repo, and the
component-token work left three files it would reformat: the heading-token block
in defaults.css, a wrapped condition in admin.js and the new inventory
assertions. No behaviour changes.
psalm read `TokenRegistry::$componentTokens` as a shape without `global` while
getComponentTokens() assigned one with it — InvalidPropertyAssignmentValue at
the assignment. The property and getComponentTokens() now declare it, because
the component layer always sets it; getTokens() declares it optional, because it
merges in the brand layer, which carries no global of its own.
Adding the toggle pushed Admin::getForm() to 109 lines against phpmd's threshold
of 100, and named a local `$primaryDrivesComponents`, which is over its
twenty-character limit.

Every toggle on this panel is stored the same way — '1' or '0' under the app id
— and getForm() read five of them as five identical five-line blocks.
`isFlagOn()` gives that shape one name: the method drops to 89 lines, the
default stays visible at the call site, which is the part that differs between
them, and the local becomes `$drivesComponents`.

The controller's parameter keeps its name and takes a documented
`@SuppressWarnings(PHPMD.LongVariable)` instead: Nextcloud binds that parameter
from the JSON body key the admin panel posts, so shortening it to satisfy a
length rule would rename the API field.
The layer was injected as a step of its own between the design-system layers and
the admin's overrides. That put it outside `designSystemLayers()`, which has two
consequences the unit suite caught: stock Nextcloud (design system `none`, which
returns before any layer is added) started loading a Thematiq stylesheet, and
`getStylesheetManifest()` never listed the layer — so the client that applies a
token set without a reload could neither add nor remove it.

It is now the last entry of `designSystemLayers()`, which is the same position in
the emitted order — after the layers whose `:root` declarations it captures,
before the overrides that may move the values those captures resolve to — while
`none` stays stock and the manifest carries it.

The three order assertions gain `component-scopes` where they already listed
every other layer.
Hydra gate-65 failed on the drift the bump opened: info.xml advertises NC 32-35
to the App Store while the matrix ran 32, 33 and 34, so the newest major was
claimed and exercised by nothing. The gate offers deleting the pin so the shared
workflow derives the range, but ClaimAccuracyTest requires the pin to be
explicit — inheriting the default is how the floor and the matrix drifted apart
in #241/#242 — so the leg is added instead.

stable35 goes first because Playwright reads `[0]` and
selector-liveness.spec.ts only expires its SINCE deferrals once the survey
reaches MAX_SUPPORTED_NC, which it reads from info.xml. Leaving stable34 in
front of a declared 35 would put the survey one major behind the claim.

Two ISimpleFolder fakes then have to load on that leg: `getOrCreateFolder()` is
@SInCE 35.0.0, and without it the suite fatals at class-load time rather than
failing a test. FontServiceTest's fake is filesystem-backed, so it creates the
directory; ThemingAuditServiceTest's is flat — the audit log lives in one folder
— so it refuses the way its newFolder() already does. Verified against a real
Nextcloud 35: 840 tests load and pass.
The comment added with the stable35 leg said the constant is read from
info.xml. It is not: it is hand-maintained in
tests/e2e/spec-coverage/selector-liveness.spec.ts and still reads 34. Raising it
expires that file's SINCE deferrals, so it is a decision of its own rather than
something the version bump carries along.
`ISimpleFolder::getOrCreateFolder()` is @SInCE 35.0.0. Both fakes in this suite
implement that interface and neither declared the method, so on a Nextcloud 35
server PHP refuses the class itself:

    Class ...FontServiceFakeSimpleFolder contains 1 abstract method and must
    therefore be declared abstract or implement the remaining method

That is a fatal at class-load time, not a test failure. The suite stops before a
single test runs, which is why an NC 35 leg cannot even produce a coverage
baseline for the ratchet to compare against — the run has no clover report to
show, and the ratchet refuses to report a number it did not measure.

FontServiceTest's fake is backed by a real temp directory, so get-or-create is
what it says: mkdir when the path is absent, then hand back a folder for it.
ThemingAuditServiceTest's fake is flat — the audit log lives in one folder and
the service never nests — so it refuses the way its own newFolder() already
does, rather than pretending to a shape it does not have.

Nothing else in the suite is NC 35-incompatible: against a real Nextcloud 35,
all 840 tests load and run.
test: let the ISimpleFolder fakes load on Nextcloud 35
CustomSniffs.Functions.NamedParameters requires named arguments on calls to this
app's own code, and the helper the phpmd fix introduced was called positionally
five times, so phpcs failed on a file the phpmd fix had just made pass.

Its second parameter is renamed $default -> $fallback so the named form does not
sit on a PHP keyword. The IConfig::getAppValue() call inside the helper stays
positional: that is Nextcloud's code, which the sniff does not cover, and `app:`
is not even its parameter name.
rubenvdlinde and others added 30 commits September 26, 2026 09:22
docs(parity): capability matrix for thematiq against five theming competitors
…ess a vendor source is cited

82 rated cells carried evidence starting with 'not checked'. 78 go to
unknown with the existing reason; 4 keep their rating and cite the vendor
source their text already quoted or linked. Thematiq's own column untouched.
…hecked-2026-09-26

fix(parity): rate thematiq's not-checked competitor cells unknown unless a vendor source is cited
…ect, eight mined roadmap and release-note rows (wip)
…opendesk, tokens-studio and liferay-dxp (wip)
…t, seven mined rows, theme switch rated in three columns (wip)
docs(parity): thematiq competitor columns re-read from source and public docs, sources objects, 36 demand-signal rows
… 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

This branch has not been deployed

No deployments
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.

2 participants