From 8080ef383a6630ad395e39b5c25f22f15c2c2e4b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2026 07:28:22 +0000 Subject: [PATCH 01/72] chore(deps): Bump @gouvfr/dsfr from 1.15.2 to 1.15.3 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](https://github.com/GouvernementFR/dsfr/compare/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] --- package-lock.json | 11 ++++++----- package.json | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4500cd49..c2345e51 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,8 @@ "version": "0.1.0", "license": "EUPL-1.2", "dependencies": { - "@fontsource/fira-sans": "^5.0.0" + "@fontsource/fira-sans": "^5.0.0", + "@gouvfr/dsfr": "1.15.3" }, "devDependencies": { "@conduction/nextcloud-vue": "^2.52.0", @@ -33,7 +34,7 @@ "npm": "^11.0.0" }, "optionalDependencies": { - "@gouvfr/dsfr": "^1.15.1" + "@gouvfr/dsfr": "^1.15.3" } }, "node_modules/@asamuzakjp/css-color": { @@ -935,9 +936,9 @@ } }, "node_modules/@gouvfr/dsfr": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@gouvfr/dsfr/-/dsfr-1.15.2.tgz", - "integrity": "sha512-IrV/a/eV6KE06Az8Ky3wN2h0INka+ntk5UWuJg/T+ST7/tMUvIWC8fZi9azx+2YsqMFGgAkocIHxeVbPoLHAUQ==", + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@gouvfr/dsfr/-/dsfr-1.15.3.tgz", + "integrity": "sha512-R9AALZc1LfZjtytWZMOaUIOHWTkllBZ/sZlsL8TpKZVgF3qUKXt/uNYh+L1D1Fh4p92ZqWxbtNcebsigBZe5pQ==", "hasInstallScript": true, "license": "etalab-2.0", "optional": true, diff --git a/package.json b/package.json index 9885af6d..a4ca49ad 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "@fontsource/fira-sans": "^5.0.0" }, "optionalDependencies": { - "@gouvfr/dsfr": "^1.15.1" + "@gouvfr/dsfr": "^1.15.3" }, "devDependencies": { "@conduction/nextcloud-vue": "^2.52.0", From 71a8f067253d2934245b15bc498980c1aff4c093 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:27:43 +0000 Subject: [PATCH 02/72] chore(deps-dev): Bump twig/twig from 3.28.0 to 3.29.0 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](https://github.com/twigphp/Twig/compare/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] --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 5d8d2af6..3a9552b6 100644 --- a/composer.lock +++ b/composer.lock @@ -7755,16 +7755,16 @@ }, { "name": "twig/twig", - "version": "v3.28.0", + "version": "v3.29.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b" + "reference": "45a3c6e9224c3377a39c7b150bb29d5d97d2c75d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/597c12ed286fb9d1701a36684ce6e0cbe28ebc8b", - "reference": "597c12ed286fb9d1701a36684ce6e0cbe28ebc8b", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/45a3c6e9224c3377a39c7b150bb29d5d97d2c75d", + "reference": "45a3c6e9224c3377a39c7b150bb29d5d97d2c75d", "shasum": "" }, "require": { @@ -7819,7 +7819,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.28.0" + "source": "https://github.com/twigphp/Twig/tree/v3.29.0" }, "funding": [ { @@ -7831,7 +7831,7 @@ "type": "tidelift" } ], - "time": "2026-07-03T20:44:34+00:00" + "time": "2026-09-18T09:10:14+00:00" }, { "name": "vimeo/psalm", From fe2f1457e94cbd2ac3235cc6e4ee1711f4a8b51c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:27:50 +0000 Subject: [PATCH 03/72] chore(deps-dev): Bump nextcloud/ocp from 34.0.4 to 35.0.0 Bumps [nextcloud/ocp](https://github.com/nextcloud-deps/ocp) from 34.0.4 to 35.0.0. - [Commits](https://github.com/nextcloud-deps/ocp/compare/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] --- composer.json | 2 +- composer.lock | 276 +++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 261 insertions(+), 17 deletions(-) diff --git a/composer.json b/composer.json index 4bfa7f6c..888fdcc8 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ "conduction/hydra-gates": "^1.8.2", "cyclonedx/cyclonedx-php-composer": "^6.2", "edgedesign/phpqa": "^1.27", - "nextcloud/ocp": "^34.0", + "nextcloud/ocp": "^35.0", "phpcsstandards/phpcsextra": "^1.4", "phpmd/phpmd": "^2.15", "phpmetrics/phpmetrics": "^2.8", diff --git a/composer.lock b/composer.lock index 5d8d2af6..86114ff7 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "09674095784df6bd8ef3f0c7c8c2573c", + "content-hash": "11071185487184c005334265eef94abf", "packages": [], "packages-dev": [ { @@ -1929,30 +1929,34 @@ }, { "name": "nextcloud/ocp", - "version": "v34.0.4", + "version": "v35.0.0", "source": { "type": "git", "url": "https://github.com/nextcloud-deps/ocp.git", - "reference": "f316bffe9b506532d1aafcf817d8d17f3bbe2cde" + "reference": "94d85a0ba6b3b3911c25b2eddb0ca0fe5691acbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/f316bffe9b506532d1aafcf817d8d17f3bbe2cde", - "reference": "f316bffe9b506532d1aafcf817d8d17f3bbe2cde", + "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/94d85a0ba6b3b3911c25b2eddb0ca0fe5691acbf", + "reference": "94d85a0ba6b3b3911c25b2eddb0ca0fe5691acbf", "shasum": "" }, "require": { - "php": "~8.2 || ~8.3 || ~8.4 || ~8.5", + "php": "~8.3 || ~8.4 || ~8.5", "psr/clock": "^1.0", "psr/container": "^2.0.2", "psr/event-dispatcher": "^1.0", "psr/http-client": "^1.0.3", - "psr/log": "^3.0.2" + "psr/log": "^3.0.2", + "symfony/polyfill-intl-normalizer": "^1.38", + "symfony/polyfill-php84": "^1.38", + "symfony/polyfill-php85": "^1.41", + "symfony/polyfill-php86": "^1.41" }, "type": "library", "extra": { "branch-alias": { - "dev-stable34": "34.0.0-dev" + "dev-stable35": "35.0.0-dev" } }, "notification-url": "https://packagist.org/downloads/", @@ -1972,9 +1976,9 @@ "description": "Composer package containing Nextcloud's public OCP API and the unstable NCU API", "support": { "issues": "https://github.com/nextcloud-deps/ocp/issues", - "source": "https://github.com/nextcloud-deps/ocp/tree/v34.0.4" + "source": "https://github.com/nextcloud-deps/ocp/tree/v35.0.0" }, - "time": "2026-09-02T01:51:50+00:00" + "time": "2026-09-04T01:52:36+00:00" }, { "name": "nikic/php-parser", @@ -7055,16 +7059,16 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.38.0", + "version": "v1.42.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", - "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502", "shasum": "" }, "require": { @@ -7116,7 +7120,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0" }, "funding": [ { @@ -7136,7 +7140,7 @@ "type": "tidelift" } ], - "time": "2026-05-25T13:48:31+00:00" + "time": "2026-08-07T06:33:24+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -7303,6 +7307,246 @@ ], "time": "2026-05-26T12:45:58+00:00" }, + { + "name": "symfony/polyfill-php84", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-01T12:47:55+00:00" + }, + { + "name": "symfony/polyfill-php86", + "version": "v1.41.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php86.git", + "reference": "6bc356ed3d8dbfeea8f0de235e34d670704e880e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/6bc356ed3d8dbfeea8f0de235e34d670704e880e", + "reference": "6bc356ed3d8dbfeea8f0de235e34d670704e880e", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php86\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php86/tree/v1.41.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-07-02T13:42:24+00:00" + }, { "name": "symfony/process", "version": "v6.4.41", From fe2e6b2f98e1b1e67e3dda90ef1ee2d535f9cd2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:28:03 +0000 Subject: [PATCH 04/72] chore(deps-dev): Bump vitest from 5.0.0 to 5.0.1 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] --- package-lock.json | 82 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4500cd49..0e545ed8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "ts-node": "^10.9.2", "typescript": "^7.0.2", "vite": "^8.3.0", - "vitest": "^5.0.0" + "vitest": "^5.0.1" }, "engines": { "node": "^22.14 || ^24 || >=26", @@ -1011,9 +1011,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, @@ -2712,9 +2712,9 @@ "license": "ISC" }, "node_modules/@vitest/coverage-v8": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.0.tgz", - "integrity": "sha512-toMg6PZGCIa/lQNCDoASrfb1ly4hsUKXFtFYC9kD4t78o5Y6LyNJU7AENt8eHPr3quYdxaxK7hj2mnbFfUk9NA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.1.tgz", + "integrity": "sha512-FRC8ACiudC3dI6MTplzRSYWHDRnIv2IPfbzs4FdoJNsMal/35sWV8hwIfV8ZcqzSPy+uXHeMVONt9CEqtOU17w==", "dev": true, "license": "MIT", "dependencies": { @@ -2731,8 +2731,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "5.0.0", - "vitest": "5.0.0" + "@vitest/browser": "5.0.1", + "vitest": "5.0.1" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2764,14 +2764,14 @@ } }, "node_modules/@vitest/mocker": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.0.tgz", - "integrity": "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz", + "integrity": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.31", - "@vitest/spy": "5.0.0", + "@vitest/spy": "5.0.1", "estree-walker": "^3.0.3", "magic-string": "^1.2.3" }, @@ -2792,19 +2792,19 @@ } }, "node_modules/@vitest/mocker/node_modules/magic-string": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.2.3.tgz", - "integrity": "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", + "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, "node_modules/@vitest/pretty-format": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-5.0.0.tgz", - "integrity": "sha512-PVRNuB3wpReb4SQEs4zTKM4KWFhQ5pw3spE8naoDJNB5T5aWRzGKHwXcLUllr0WeOTXpB6bSr3CJLo5+7XQSSQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-5.0.1.tgz", + "integrity": "sha512-6guWwj5d9bguuefTOvJoq387tfpkzSv554YdUEGzjJH2PnnmvzTLQ1UQSuAk5wBFVhf2CUmy/S/palOcb6dmpA==", "dev": true, "license": "MIT", "dependencies": { @@ -2815,9 +2815,9 @@ } }, "node_modules/@vitest/spy": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.0.tgz", - "integrity": "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.1.tgz", + "integrity": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==", "dev": true, "license": "MIT", "funding": { @@ -2825,13 +2825,13 @@ } }, "node_modules/@vitest/ui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-5.0.0.tgz", - "integrity": "sha512-h2FIFwggCY2GxUd2UdQoYNVQkOIqEQLPhNREcl3FUiRsdzQep7NWwYbSmhGEA9nFLPDq5pXzRMcBZQU8Py83sg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-5.0.1.tgz", + "integrity": "sha512-7PvQu/X9/pQoHYfNLAYL22qsD4/+sx2k7zpUA7XvjW0sc40r3/r0lGZ2fsEyOHMuO8Q1KjiO1QQVjJdnWUy/WQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "5.0.0", + "@vitest/utils": "5.0.1", "fflate": "^0.8.3", "flatted": "^3.4.4", "pathe": "^2.0.3", @@ -2842,17 +2842,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "5.0.0" + "vitest": "5.0.1" } }, "node_modules/@vitest/utils": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-5.0.0.tgz", - "integrity": "sha512-dO++xL3vDfvhTAVimfkuQUA3k+JClIF1i1vAkPqpcGAthRmeWnXmHB7YPViPvgCwviX8u7Y5W1u2N//AaQr3fw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-5.0.1.tgz", + "integrity": "sha512-E9+yEA+jsfaoxZcUHFzEqUrQcoNh2EwrPT5efIqkUPUwD5Ua2Li9BRWaYeRwvzvdLgTSVrre8oKNeyrfg7KkdQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "5.0.0", + "@vitest/pretty-format": "5.0.1", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.1" }, @@ -10457,14 +10457,14 @@ } }, "node_modules/vitest": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.0.tgz", - "integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz", + "integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/mocker": "5.0.0", + "@vitest/mocker": "5.0.1", "chai": "^6.2.2", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", @@ -10490,12 +10490,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "5.0.0", - "@vitest/browser-preview": "5.0.0", + "@vitest/browser-playwright": "5.0.1", + "@vitest/browser-preview": "5.0.1", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", - "@vitest/coverage-istanbul": "5.0.0", - "@vitest/coverage-v8": "5.0.0", - "@vitest/ui": "5.0.0", + "@vitest/coverage-istanbul": "5.0.1", + "@vitest/coverage-v8": "5.0.1", + "@vitest/ui": "5.0.1", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index d43fb39c..cb16aa8b 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "ts-node": "^10.9.2", "typescript": "^7.0.2", "vite": "^8.3.0", - "vitest": "^5.0.0" + "vitest": "^5.0.1" }, "prettier": "@nextcloud/prettier-config" } From 1515d27963df964445399c4819193447c99bc035 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:28:12 +0000 Subject: [PATCH 05/72] chore(deps-dev): Bump @vitest/coverage-v8 from 5.0.0 to 5.0.1 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] --- package-lock.json | 82 +++++++++++++++++++++++------------------------ package.json | 2 +- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4500cd49..7a3e6670 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@nextcloud/prettier-config": "^1.2.0", "@openfun/cunningham-tokens": "^3.0.0", "@playwright/test": "^1.63.0", - "@vitest/coverage-v8": "^5.0.0", + "@vitest/coverage-v8": "^5.0.1", "@vitest/ui": "^5.0.0", "jsdom": "^30.0.1", "prettier": "^3.9.6", @@ -1011,9 +1011,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "dev": true, "license": "MIT" }, @@ -2712,9 +2712,9 @@ "license": "ISC" }, "node_modules/@vitest/coverage-v8": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.0.tgz", - "integrity": "sha512-toMg6PZGCIa/lQNCDoASrfb1ly4hsUKXFtFYC9kD4t78o5Y6LyNJU7AENt8eHPr3quYdxaxK7hj2mnbFfUk9NA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-5.0.1.tgz", + "integrity": "sha512-FRC8ACiudC3dI6MTplzRSYWHDRnIv2IPfbzs4FdoJNsMal/35sWV8hwIfV8ZcqzSPy+uXHeMVONt9CEqtOU17w==", "dev": true, "license": "MIT", "dependencies": { @@ -2731,8 +2731,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "5.0.0", - "vitest": "5.0.0" + "@vitest/browser": "5.0.1", + "vitest": "5.0.1" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -2764,14 +2764,14 @@ } }, "node_modules/@vitest/mocker": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.0.tgz", - "integrity": "sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.1.tgz", + "integrity": "sha512-6K1DoBNAPGvuOcSsGA4D6x+5zEEff/KmOOP3uetT2TrGpVfI+HRHRnJJfKi5ib/g1vx8IYHQD8s0pbJz8WQI7Q==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.31", - "@vitest/spy": "5.0.0", + "@vitest/spy": "5.0.1", "estree-walker": "^3.0.3", "magic-string": "^1.2.3" }, @@ -2792,19 +2792,19 @@ } }, "node_modules/@vitest/mocker/node_modules/magic-string": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.2.3.tgz", - "integrity": "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==", + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.1.tgz", + "integrity": "sha512-8lyCu36ErXR0J9uaGKlKQoiLZKmtI63YGLE8G2o9jyRPdr4X47LusSOwgOJOzcVtp81fTAAjxR7BwKz682Jhow==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, "node_modules/@vitest/pretty-format": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-5.0.0.tgz", - "integrity": "sha512-PVRNuB3wpReb4SQEs4zTKM4KWFhQ5pw3spE8naoDJNB5T5aWRzGKHwXcLUllr0WeOTXpB6bSr3CJLo5+7XQSSQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-5.0.1.tgz", + "integrity": "sha512-6guWwj5d9bguuefTOvJoq387tfpkzSv554YdUEGzjJH2PnnmvzTLQ1UQSuAk5wBFVhf2CUmy/S/palOcb6dmpA==", "dev": true, "license": "MIT", "dependencies": { @@ -2815,9 +2815,9 @@ } }, "node_modules/@vitest/spy": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.0.tgz", - "integrity": "sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.1.tgz", + "integrity": "sha512-rbto/mF/SGERxEgYOek7Xm6B9b+y+mVoo+f4b2LymYO8zM1b7uB5nHuhVMTP2hxdzgxvGiZYGxGIaMvL5y180Q==", "dev": true, "license": "MIT", "funding": { @@ -2825,13 +2825,13 @@ } }, "node_modules/@vitest/ui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-5.0.0.tgz", - "integrity": "sha512-h2FIFwggCY2GxUd2UdQoYNVQkOIqEQLPhNREcl3FUiRsdzQep7NWwYbSmhGEA9nFLPDq5pXzRMcBZQU8Py83sg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-5.0.1.tgz", + "integrity": "sha512-7PvQu/X9/pQoHYfNLAYL22qsD4/+sx2k7zpUA7XvjW0sc40r3/r0lGZ2fsEyOHMuO8Q1KjiO1QQVjJdnWUy/WQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "5.0.0", + "@vitest/utils": "5.0.1", "fflate": "^0.8.3", "flatted": "^3.4.4", "pathe": "^2.0.3", @@ -2842,17 +2842,17 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "5.0.0" + "vitest": "5.0.1" } }, "node_modules/@vitest/utils": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-5.0.0.tgz", - "integrity": "sha512-dO++xL3vDfvhTAVimfkuQUA3k+JClIF1i1vAkPqpcGAthRmeWnXmHB7YPViPvgCwviX8u7Y5W1u2N//AaQr3fw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-5.0.1.tgz", + "integrity": "sha512-E9+yEA+jsfaoxZcUHFzEqUrQcoNh2EwrPT5efIqkUPUwD5Ua2Li9BRWaYeRwvzvdLgTSVrre8oKNeyrfg7KkdQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "5.0.0", + "@vitest/pretty-format": "5.0.1", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.1" }, @@ -10457,14 +10457,14 @@ } }, "node_modules/vitest": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.0.tgz", - "integrity": "sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.1.tgz", + "integrity": "sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/mocker": "5.0.0", + "@vitest/mocker": "5.0.1", "chai": "^6.2.2", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", @@ -10490,12 +10490,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "5.0.0", - "@vitest/browser-preview": "5.0.0", + "@vitest/browser-playwright": "5.0.1", + "@vitest/browser-preview": "5.0.1", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", - "@vitest/coverage-istanbul": "5.0.0", - "@vitest/coverage-v8": "5.0.0", - "@vitest/ui": "5.0.0", + "@vitest/coverage-istanbul": "5.0.1", + "@vitest/coverage-v8": "5.0.1", + "@vitest/ui": "5.0.1", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" diff --git a/package.json b/package.json index d43fb39c..917131ef 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "@nextcloud/prettier-config": "^1.2.0", "@openfun/cunningham-tokens": "^3.0.0", "@playwright/test": "^1.63.0", - "@vitest/coverage-v8": "^5.0.0", + "@vitest/coverage-v8": "^5.0.1", "@vitest/ui": "^5.0.0", "jsdom": "^30.0.1", "prettier": "^3.9.6", From c7523e8cb54c6a2dd45626b8ecf3fad3fe2e3e9a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:28:34 +0000 Subject: [PATCH 06/72] chore(deps-dev): Bump @conduction/nextcloud-vue from 2.52.0 to 2.55.1 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](https://github.com/ConductionNL/nextcloud-vue/compare/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] --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4500cd49..e79bd49f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@fontsource/fira-sans": "^5.0.0" }, "devDependencies": { - "@conduction/nextcloud-vue": "^2.52.0", + "@conduction/nextcloud-vue": "^2.55.1", "@cyclonedx/cyclonedx-npm": "^6.0.1", "@nextcloud/prettier-config": "^1.2.0", "@openfun/cunningham-tokens": "^3.0.0", @@ -449,9 +449,9 @@ } }, "node_modules/@conduction/nextcloud-vue": { - "version": "2.52.0", - "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.52.0.tgz", - "integrity": "sha512-c0EjYVZy2RxJjJAQxqXz7MobR5xaxWqKXgjkrROSwDfKgns6cAvOjyaKEJQs4740C5QjcMmqg9nmlIcSFzseNA==", + "version": "2.55.1", + "resolved": "https://registry.npmjs.org/@conduction/nextcloud-vue/-/nextcloud-vue-2.55.1.tgz", + "integrity": "sha512-QIM7xZHg3odrULrB+AUPq4uDLQ2eBKcpRf3dsMCBSjmLA58RQTj25EC3nlfR/AJzr4KuvS3Ai95UPM4E/2tfIw==", "dev": true, "license": "EUPL-1.2", "dependencies": { diff --git a/package.json b/package.json index d43fb39c..c280f6a1 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@gouvfr/dsfr": "^1.15.1" }, "devDependencies": { - "@conduction/nextcloud-vue": "^2.52.0", + "@conduction/nextcloud-vue": "^2.55.1", "@cyclonedx/cyclonedx-npm": "^6.0.1", "@nextcloud/prettier-config": "^1.2.0", "@openfun/cunningham-tokens": "^3.0.0", From af5953e4cecf02a939afdb13f61905f7fa19a05b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:28:41 +0000 Subject: [PATCH 07/72] chore(deps-dev): Bump jsdom from 30.0.1 to 30.1.0 Bumps [jsdom](https://github.com/jsdom/jsdom) from 30.0.1 to 30.1.0. - [Release notes](https://github.com/jsdom/jsdom/releases) - [Commits](https://github.com/jsdom/jsdom/compare/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] --- package-lock.json | 80 +++++++++++++++++++++-------------------------- package.json | 2 +- 2 files changed, 37 insertions(+), 45 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4500cd49..9ebdd5ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "@playwright/test": "^1.63.0", "@vitest/coverage-v8": "^5.0.0", "@vitest/ui": "^5.0.0", - "jsdom": "^30.0.1", + "jsdom": "^30.1.0", "prettier": "^3.9.6", "stylelint": "^17.15.0", "stylelint-config-standard": "^40.0.0", @@ -37,36 +37,36 @@ } }, "node_modules/@asamuzakjp/css-color": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", - "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-7.0.0.tgz", + "integrity": "sha512-IxddtGnnoidVSO6O/aiTueptbTThHvi2qGHu9/AWuD1J7i86Ox+jl+Y5SXcDH1RixzQlP2cjH/PjnlImZiAurQ==", "dev": true, "license": "MIT", "dependencies": { "@csstools/css-calc": "^3.3.0", - "@csstools/css-color-parser": "^4.1.10", + "@csstools/css-color-parser": "^4.2.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.5.2" }, "engines": { - "node": "^22.13.0 || >=24.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", - "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-9.2.1.tgz", + "integrity": "sha512-NT4s3yZLjovPpliRpTvdsdzyPjqRqiCZj9MxnarBihbaY5MbAG7DWAJcrLlhmC7xKTNCXsTELcqB8YsqpLnUFQ==", "dev": true, "license": "MIT", "dependencies": { - "bidi-js": "^1.0.3", + "bidi-js": "^1.1.0", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.5.2" + "lru-cache": "^11.5.3" }, "engines": { - "node": "^22.13.0 || >=24.0.0" + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" } }, "node_modules/@babel/code-frame": { @@ -587,9 +587,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", - "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.4.0.tgz", + "integrity": "sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==", "dev": true, "funding": [ { @@ -611,9 +611,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.2.tgz", - "integrity": "sha512-3QKjR/vxyjcSXBLgb6lP0S3MGdvwbmqSsvLPbYdVORqPDc8FX1HAJ0Spk38bxaRXgvENTA47tlhhbb5Z2e8hEg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.3.tgz", + "integrity": "sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==", "dev": true, "funding": [ { @@ -628,7 +628,7 @@ "license": "MIT", "dependencies": { "@csstools/color-helpers": "^6.1.1", - "@csstools/css-calc": "^3.3.0" + "@csstools/css-calc": "^3.4.0" }, "engines": { "node": ">=20.19.0" @@ -662,9 +662,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.12.tgz", - "integrity": "sha512-3vLQK+dXxhBMR2Wx99PTCifE+vHtW2ndZWyla8yK813ev6oGhyn8Lja8jCyGAWTJ+LEYZK7EVtJxrDj8ztevJw==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.14.tgz", + "integrity": "sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==", "dev": true, "funding": [ { @@ -5936,16 +5936,16 @@ } }, "node_modules/jsdom": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", - "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.1.0.tgz", + "integrity": "sha512-h/Q+H+KmlGf49ITw3TrykSVbg04S75S2O7ZtJc9gMGOWBrQRZmx5AJIKLtJ01e6Kszl+/tpFthCVqxNifxhoqg==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^6.0.5", - "@asamuzakjp/dom-selector": "^8.3.0", + "@asamuzakjp/css-color": "^7.0.0", + "@asamuzakjp/dom-selector": "^9.1.2", "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@csstools/css-syntax-patches-for-csstree": "^1.1.13", "@exodus/bytes": "^1.15.1", "css-tree": "^3.2.1", "data-urls": "^7.0.0", @@ -5955,13 +5955,12 @@ "lru-cache": "^11.5.2", "parse5": "^8.0.1", "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.2", - "undici": "^8.9.0", + "undici": "^8.10.2", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^17.1.0", + "whatwg-url": "^17.1.1", "xml-name-validator": "^5.0.0" }, "engines": { @@ -5977,9 +5976,9 @@ } }, "node_modules/jsdom/node_modules/whatwg-url": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", - "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "version": "17.1.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.1.tgz", + "integrity": "sha512-ohjk1mdUebJVadRt3bAhQhx8lSnISq+GDttK79LFl8EHQkAPvzwctoasC4hs8tBt6kLAncBWWyq1N52qEfKvDw==", "dev": true, "license": "MIT", "dependencies": { @@ -6422,9 +6421,9 @@ } }, "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -9621,13 +9620,6 @@ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", "dev": true }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, "node_modules/tabbable": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", diff --git a/package.json b/package.json index d43fb39c..fc6a0b9b 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "@playwright/test": "^1.63.0", "@vitest/coverage-v8": "^5.0.0", "@vitest/ui": "^5.0.0", - "jsdom": "^30.0.1", + "jsdom": "^30.1.0", "prettier": "^3.9.6", "stylelint": "^17.15.0", "stylelint-config-standard": "^40.0.0", From 1993b287eb6da824ef3994cdff18df2ae7ccffbe Mon Sep 17 00:00:00 2001 From: Remko Date: Wed, 23 Sep 2026 13:22:47 +0200 Subject: [PATCH 08/72] feat(tokens): add the component token mapping and its generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .prettierignore | 7 + css/component-scopes.css | 832 +++++++++++++++ css/primary-lock.css | 67 ++ package.json | 4 +- scripts/generate-component-scopes.mjs | 370 +++++++ scripts/mapping/component-tokens.json | 1372 +++++++++++++++++++++++++ 6 files changed, 2651 insertions(+), 1 deletion(-) create mode 100644 css/component-scopes.css create mode 100644 css/primary-lock.css create mode 100644 scripts/generate-component-scopes.mjs create mode 100644 scripts/mapping/component-tokens.json diff --git a/.prettierignore b/.prettierignore index 843e68f5..c2901175 100644 --- a/.prettierignore +++ b/.prettierignore @@ -52,3 +52,10 @@ scripts/sources/ # file — so the validation tests have something to reject. Formatting them # would remove the very defect under test. tests/integration/fixtures/ + +# Same contract again: both emitted by scripts/generate-component-scopes.mjs +# from scripts/mapping/component-tokens.json and byte-compared against the +# committed copies by `npm run test:component-scopes`. Prettier would rewrap +# the var() fallback chains and fail that check on a file nobody edited. +css/component-scopes.css +css/primary-lock.css diff --git a/css/component-scopes.css b/css/component-scopes.css new file mode 100644 index 00000000..c11c4341 --- /dev/null +++ b/css/component-scopes.css @@ -0,0 +1,832 @@ +/** + * GENERATED FILE — do not edit. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * Source: scripts/mapping/component-tokens.json + * Regenerate: npm run generate:component-scopes + */ + +/* + * Component-scoped tokens. + * + * Each rule below redeclares, inside one component's subtree, the Nextcloud + * variable that component consumes — so the component follows its own token + * when one is set and the brand-wide global when one is not. Nextcloud's own + * stylesheets keep doing the painting; only the value they see changes. + * + * The :root block captures each global under a name the scopes can fall back + * to. Falling back to the global directly would be a self-referential custom + * property, which CSS discards as invalid. + */ + +:root { + --thematiq-global-color-primary: var(--color-primary); + --thematiq-global-color-primary-text: var(--color-primary-text); + --thematiq-global-color-background-hover: var(--color-background-hover); + --thematiq-global-border-radius-large: var(--border-radius-large); + --thematiq-global-color-border: var(--color-border); + --thematiq-global-color-primary-element: var(--color-primary-element); + --thematiq-global-color-primary-element-text: var(--color-primary-element-text); + --thematiq-global-color-primary-element-hover: var(--color-primary-element-hover); + --thematiq-global-border-radius-element: var(--border-radius-element); + --thematiq-global-font-face: var(--font-face); + --thematiq-global-color-main-text: var(--color-main-text); + --thematiq-global-body-container-radius: var(--body-container-radius); + --thematiq-global-border-radius-rounded: var(--border-radius-rounded); + --thematiq-global-color-scrollbar: var(--color-scrollbar); + --thematiq-global-color-text-maxcontrast: var(--color-text-maxcontrast); + --thematiq-global-color-background-dark: var(--color-background-dark); + --thematiq-global-color-border-dark: var(--color-border-dark); + --thematiq-global-animation-quick: var(--animation-quick); + --thematiq-global-color-primary-light: var(--color-primary-light); + --thematiq-global-color-primary-light-text: var(--color-primary-light-text); + --thematiq-global-color-primary-light-hover: var(--color-primary-light-hover); + --thematiq-global-color-success: var(--color-success); + --thematiq-global-color-border-maxcontrast: var(--color-border-maxcontrast); + --thematiq-global-color-placeholder-dark: var(--color-placeholder-dark); + --thematiq-global-color-placeholder-light: var(--color-placeholder-light); + --thematiq-global-color-error: var(--color-error); + --thematiq-global-border-radius-small: var(--border-radius-small); + --thematiq-global-border-radius: var(--border-radius); + --thematiq-global-animation-slow: var(--animation-slow); + --thematiq-global-color-background-darker: var(--color-background-darker); + --thematiq-global-border-radius-pill: var(--border-radius-pill); + --thematiq-global-font-weight-element: var(--font-weight-element); + --thematiq-global-color-primary-element-light-text: var(--color-primary-element-light-text); + --thematiq-global-color-primary-element-light: var(--color-primary-element-light); + --thematiq-global-color-primary-element-light-hover: var(--color-primary-element-light-hover); + --thematiq-global-color-error-rgb: var(--color-error-rgb); + --thematiq-global-color-error-hover: var(--color-error-hover); + --thematiq-global-color-element-success: var(--color-element-success); + --thematiq-global-color-info: var(--color-info); + --thematiq-global-color-element-info: var(--color-element-info); + --thematiq-global-color-warning: var(--color-warning); + --thematiq-global-color-element-warning: var(--color-element-warning); + --thematiq-global-color-warning-rgb: var(--color-warning-rgb); + --thematiq-global-color-element-error: var(--color-element-error); + --thematiq-global-color-border-error: var(--color-border-error); + --thematiq-global-color-border-success: var(--color-border-success); + --thematiq-global-color-success-rgb: var(--color-success-rgb); + --thematiq-global-color-favorite: var(--color-favorite); + --thematiq-global-font-weight-heading: var(--font-weight-heading); + --thematiq-global-font-weight-default: var(--font-weight-default); + --thematiq-global-color-primary-hover: var(--color-primary-hover); + --thematiq-global-color-text-light: var(--color-text-light); + --thematiq-global-color-text-lighter: var(--color-text-lighter); + --thematiq-global-color-text-error: var(--color-text-error); + --thematiq-global-color-text-warning: var(--color-text-warning); + --thematiq-global-color-text-success: var(--color-text-success); + + /* Component tokens that a component redirects onto its own. */ + --thematiq-alias-nldesign-component-button-primary-action-background-color: var(--nldesign-component-button-primary-action-background-color); + --thematiq-alias-nldesign-component-button-primary-action-border-color: var(--nldesign-component-button-primary-action-border-color); + --thematiq-alias-nldesign-component-button-primary-action-color: var(--nldesign-component-button-primary-action-color); + --thematiq-alias-nldesign-component-button-primary-action-hover-background-color: var(--nldesign-component-button-primary-action-hover-background-color); + --thematiq-alias-nldesign-component-button-border-radius: var(--nldesign-component-button-border-radius); + --thematiq-alias-nldesign-component-textbox-border-color: var(--nldesign-component-textbox-border-color); + --thematiq-alias-nldesign-component-textbox-border-radius: var(--nldesign-component-textbox-border-radius); + --thematiq-alias-nldesign-component-button-primary-action-font-weight: var(--nldesign-component-button-primary-action-font-weight); +} + +/* header-bar */ +#header, +[data-thematiq-component='header-bar'] { + --color-primary: var( + --nldesign-component-header-background-color, + var(--thematiq-global-color-primary) + ); + --color-primary-text: var( + --nldesign-component-header-color, + var(--thematiq-global-color-primary-text) + ); + --color-background-hover: var( + --nldesign-component-header-hover-background-color, + var(--thematiq-global-color-background-hover) + ); +} + +/* login-card */ +#body-login .guest-box, +#body-login .login-box, +[data-thematiq-component='login-card'] { + --border-radius-large: var( + --nldesign-component-login-box-border-radius, + var(--thematiq-global-border-radius-large) + ); + --color-border: var( + --nldesign-component-login-box-border-color, + var(--thematiq-global-color-border) + ); +} + +/* login-button */ +#body-login .button-vue--primary, +#body-login .button-vue--vue-primary, +#body-login input[type='submit'], +#body-login button.primary, +.login-form button[type='submit'], +[data-thematiq-component='login-button'] { + --color-primary-element: var( + --nldesign-component-login-button-background-color, + var(--thematiq-global-color-primary-element) + ); + --color-primary-element-text: var( + --nldesign-component-login-button-color, + var(--thematiq-global-color-primary-element-text) + ); + --color-primary-element-hover: var( + --nldesign-component-login-button-hover-background-color, + var(--thematiq-global-color-primary-element-hover) + ); + --border-radius-element: var( + --nldesign-component-login-button-border-radius, + var(--thematiq-global-border-radius-element) + ); + --nldesign-component-button-primary-action-background-color: var( + --nldesign-component-login-button-background-color, + var(--thematiq-alias-nldesign-component-button-primary-action-background-color) + ); + --nldesign-component-button-primary-action-border-color: var( + --nldesign-component-login-button-background-color, + var(--thematiq-alias-nldesign-component-button-primary-action-border-color) + ); + --nldesign-component-button-primary-action-color: var( + --nldesign-component-login-button-color, + var(--thematiq-alias-nldesign-component-button-primary-action-color) + ); + --nldesign-component-button-primary-action-hover-background-color: var( + --nldesign-component-login-button-hover-background-color, + var(--thematiq-alias-nldesign-component-button-primary-action-hover-background-color) + ); + --nldesign-component-button-border-radius: var( + --nldesign-component-login-button-border-radius, + var(--thematiq-alias-nldesign-component-button-border-radius) + ); +} + +/* logo-slogan */ +#body-login .header-guest, +#body-login .logo, +[data-thematiq-component='logo-slogan'] { + --color-primary-text: var( + --nldesign-component-logo-slogan-color, + var(--thematiq-global-color-primary-text) + ); + --font-face: var( + --nldesign-component-logo-slogan-font-family, + var(--thematiq-global-font-face) + ); +} + +/* app-navigation */ +#app-navigation, +#app-navigation-vue, +.app-navigation, +[data-thematiq-component='app-navigation'] { + --border-radius-element: var( + --nldesign-component-navigation-border-radius, + var(--thematiq-global-border-radius-element) + ); + --color-main-text: var( + --nldesign-component-navigation-color, + var(--thematiq-global-color-main-text) + ); + --color-primary-element: var( + --nldesign-component-navigation-active-background-color, + var(--thematiq-global-color-primary-element) + ); +} + +/* content-card */ +#app-content, +.app-content, +#content, +[data-thematiq-component='content-card'] { + --body-container-radius: var( + --nldesign-component-content-card-container-border-radius, + var(--thematiq-global-body-container-radius) + ); + --border-radius-rounded: var( + --nldesign-component-content-card-border-radius, + var(--thematiq-global-border-radius-rounded) + ); + --color-border: var( + --nldesign-component-content-card-border-color, + var(--thematiq-global-color-border) + ); + --color-scrollbar: var( + --nldesign-component-content-card-scrollbar-color, + var(--thematiq-global-color-scrollbar) + ); +} + +/* table */ +table, +[data-thematiq-component='table'] { + --color-text-maxcontrast: var( + --nldesign-component-table-header-color, + var(--thematiq-global-color-text-maxcontrast) + ); + --color-border: var( + --nldesign-component-table-border-color, + var(--thematiq-global-color-border) + ); + --color-background-dark: var( + --nldesign-component-table-zebra-background-color, + var(--thematiq-global-color-background-dark) + ); + --color-background-hover: var( + --nldesign-component-table-row-hover-background-color, + var(--thematiq-global-color-background-hover) + ); +} + +/* sidebar */ +#app-sidebar, +#app-sidebar-vue, +.app-sidebar, +[data-thematiq-component='sidebar'] { + --color-border: var( + --nldesign-component-sidebar-border-color, + var(--thematiq-global-color-border) + ); + --color-border-dark: var( + --nldesign-component-sidebar-divider-color, + var(--thematiq-global-color-border-dark) + ); + --border-radius-large: var( + --nldesign-component-sidebar-border-radius, + var(--thematiq-global-border-radius-large) + ); + --color-primary-element: var( + --nldesign-component-sidebar-active-color, + var(--thematiq-global-color-primary-element) + ); +} + +/* list-item */ +.list-item, +.list-item__wrapper, +[data-thematiq-component='list-item'] { + --border-radius-element: var( + --nldesign-component-list-item-border-radius, + var(--thematiq-global-border-radius-element) + ); + --color-text-maxcontrast: var( + --nldesign-component-list-item-subline-color, + var(--thematiq-global-color-text-maxcontrast) + ); + --color-background-hover: var( + --nldesign-component-list-item-hover-background-color, + var(--thematiq-global-color-background-hover) + ); + --animation-quick: var( + --nldesign-component-list-item-transition-duration, + var(--thematiq-global-animation-quick) + ); + --color-primary-light: var( + --nldesign-component-list-item-selected-background-color, + var(--thematiq-global-color-primary-light) + ); + --color-primary-light-text: var( + --nldesign-component-list-item-selected-color, + var(--thematiq-global-color-primary-light-text) + ); + --color-primary-light-hover: var( + --nldesign-component-list-item-selected-hover-background-color, + var(--thematiq-global-color-primary-light-hover) + ); +} + +/* empty-content */ +.empty-content, +[data-thematiq-component='empty-content'] { + --color-main-text: var( + --nldesign-component-empty-content-color, + var(--thematiq-global-color-main-text) + ); + --color-text-maxcontrast: var( + --nldesign-component-empty-content-description-color, + var(--thematiq-global-color-text-maxcontrast) + ); + --color-background-dark: var( + --nldesign-component-empty-content-icon-background-color, + var(--thematiq-global-color-background-dark) + ); +} + +/* actions-menu */ +.popover, +.popovermenu, +.v-popper__popper, +[data-thematiq-component='actions-menu'] { + --color-main-text: var( + --nldesign-component-actions-menu-color, + var(--thematiq-global-color-main-text) + ); + --border-radius-large: var( + --nldesign-component-actions-menu-border-radius, + var(--thematiq-global-border-radius-large) + ); + --color-background-hover: var( + --nldesign-component-actions-menu-hover-background-color, + var(--thematiq-global-color-background-hover) + ); +} + +/* avatar */ +.avatardiv, +.user-status-icon, +[data-thematiq-component='avatar'] { + --color-success: var( + --nldesign-component-avatar-online-status-color, + var(--thematiq-global-color-success) + ); +} + +/* breadcrumbs */ +.breadcrumb, +.vue-crumb, +[data-thematiq-component='breadcrumbs'] { + --color-primary: var( + --nldesign-component-breadcrumb-color, + var(--thematiq-global-color-primary) + ); + --color-text-maxcontrast: var( + --nldesign-component-breadcrumb-separator-color, + var(--thematiq-global-color-text-maxcontrast) + ); + --color-background-hover: var( + --nldesign-component-breadcrumb-hover-background-color, + var(--thematiq-global-color-background-hover) + ); +} + +/* settings-section */ +.settings-section, +.app-settings-section, +[data-thematiq-component='settings-section'] { + --color-main-text: var( + --nldesign-component-settings-section-heading-color, + var(--thematiq-global-color-main-text) + ); + --color-text-maxcontrast: var( + --nldesign-component-settings-section-hint-color, + var(--thematiq-global-color-text-maxcontrast) + ); + --color-border: var( + --nldesign-component-settings-section-border-color, + var(--thematiq-global-color-border) + ); +} + +/* text-input */ +.input-field, +input[type='text'], +input[type='password'], +input[type='email'], +input[type='search'], +[data-thematiq-component='text-input'] { + --color-border-maxcontrast: var( + --nldesign-component-textbox-border-color, + var(--thematiq-global-color-border-maxcontrast) + ); + --color-placeholder-dark: var( + --nldesign-component-textbox-placeholder-color, + var(--thematiq-global-color-placeholder-dark) + ); + --color-placeholder-light: var( + --nldesign-component-textbox-placeholder-light-color, + var(--thematiq-global-color-placeholder-light) + ); + --border-radius-element: var( + --nldesign-component-textbox-border-radius, + var(--thematiq-global-border-radius-element) + ); + --color-primary-element: var( + --nldesign-component-textbox-focus-border-color, + var(--thematiq-global-color-primary-element) + ); + --color-error: var( + --nldesign-component-textbox-invalid-border-color, + var(--thematiq-global-color-error) + ); +} + +/* select */ +select, +.v-select, +[data-thematiq-component='select'] { + --color-border-maxcontrast: var( + --nldesign-component-form-select-border-color, + var(--thematiq-global-color-border-maxcontrast) + ); + --border-radius-element: var( + --nldesign-component-form-select-border-radius, + var(--thematiq-global-border-radius-element) + ); + --color-background-hover: var( + --nldesign-component-form-select-option-hover-background-color, + var(--thematiq-global-color-background-hover) + ); +} + +/* checkbox-switch */ +.checkbox-radio-switch, +input[type='checkbox'], +input[type='radio'], +[data-thematiq-component='checkbox-switch'] { + --color-border-maxcontrast: var( + --nldesign-component-checkbox-border-color, + var(--thematiq-global-color-border-maxcontrast) + ); + --border-radius-small: var( + --nldesign-component-checkbox-border-radius, + var(--thematiq-global-border-radius-small) + ); + --color-primary-element: var( + --nldesign-component-checkbox-checked-background-color, + var(--thematiq-global-color-primary-element) + ); +} + +/* textarea */ +textarea, +[data-thematiq-component='textarea'] { + --color-border-maxcontrast: var( + --nldesign-component-textarea-border-color, + var(--thematiq-global-color-border-maxcontrast) + ); + --border-radius: var( + --nldesign-component-textarea-border-radius, + var(--thematiq-global-border-radius) + ); + --nldesign-component-textbox-border-color: var( + --nldesign-component-textarea-border-color, + var(--thematiq-alias-nldesign-component-textbox-border-color) + ); + --nldesign-component-textbox-border-radius: var( + --nldesign-component-textarea-border-radius, + var(--thematiq-alias-nldesign-component-textbox-border-radius) + ); +} + +/* dialog */ +.modal-container, +.modal-wrapper, +.dialog, +[data-thematiq-component='dialog'] { + --border-radius-large: var( + --nldesign-component-dialog-border-radius, + var(--thematiq-global-border-radius-large) + ); + --color-border: var( + --nldesign-component-dialog-border-color, + var(--thematiq-global-color-border) + ); + --animation-slow: var( + --nldesign-component-dialog-transition-duration, + var(--thematiq-global-animation-slow) + ); + --color-primary-element: var( + --nldesign-component-dialog-confirm-background-color, + var(--thematiq-global-color-primary-element) + ); +} + +/* progress */ +progress, +.progress-bar, +.icon-loading, +[data-thematiq-component='progress'] { + --color-primary-element: var( + --nldesign-component-progress-background-color, + var(--thematiq-global-color-primary-element) + ); + --color-background-darker: var( + --nldesign-component-progress-track-background-color, + var(--thematiq-global-color-background-darker) + ); + --border-radius-pill: var( + --nldesign-component-progress-border-radius, + var(--thematiq-global-border-radius-pill) + ); + --animation-slow: var( + --nldesign-component-progress-transition-duration, + var(--thematiq-global-animation-slow) + ); +} + +/* primary-button */ +.button-vue--primary, +.button-vue--vue-primary, +button.primary, +input[type='submit'], +[data-thematiq-component='primary-button'] { + --color-primary-element: var( + --nldesign-component-button-primary-action-background-color, + var(--thematiq-global-color-primary-element) + ); + --color-primary-element-text: var( + --nldesign-component-button-primary-action-color, + var(--thematiq-global-color-primary-element-text) + ); + --color-primary-element-hover: var( + --nldesign-component-button-primary-action-hover-background-color, + var(--thematiq-global-color-primary-element-hover) + ); + --color-main-text: var( + --nldesign-component-button-focus-border-color, + var(--thematiq-global-color-main-text) + ); + --border-radius-element: var( + --nldesign-component-button-border-radius, + var(--thematiq-global-border-radius-element) + ); + --font-weight-element: var( + --nldesign-component-button-font-weight, + var(--thematiq-global-font-weight-element) + ); + --nldesign-component-button-primary-action-font-weight: var( + --nldesign-component-button-font-weight, + var(--thematiq-alias-nldesign-component-button-primary-action-font-weight) + ); +} + +/* secondary-button */ +.button-vue--secondary, +.button-vue--vue-secondary, +[data-thematiq-component='secondary-button'] { + --color-background-dark: var( + --nldesign-component-button-secondary-action-background-color, + var(--thematiq-global-color-background-dark) + ); + --color-main-text: var( + --nldesign-component-button-secondary-action-color, + var(--thematiq-global-color-main-text) + ); + --color-background-darker: var( + --nldesign-component-button-secondary-action-hover-background-color, + var(--thematiq-global-color-background-darker) + ); + --border-radius-element: var( + --nldesign-component-button-secondary-action-border-radius, + var(--thematiq-global-border-radius-element) + ); + --nldesign-component-button-border-radius: var( + --nldesign-component-button-secondary-action-border-radius, + var(--thematiq-alias-nldesign-component-button-border-radius) + ); +} + +/* tertiary-button */ +.button-vue--tertiary, +.button-vue--vue-tertiary, +.button-vue--tertiary-no-background, +[data-thematiq-component='tertiary-button'] { + --color-primary-element-light-text: var( + --nldesign-component-button-color, + var(--thematiq-global-color-primary-element-light-text) + ); + --color-primary-element-light: var( + --nldesign-component-button-hover-background-color, + var(--thematiq-global-color-primary-element-light) + ); + --color-primary-element-light-hover: var( + --nldesign-component-button-active-background-color, + var(--thematiq-global-color-primary-element-light-hover) + ); +} + +/* error-button */ +.button-vue--error, +.button-vue--vue-error, +[data-thematiq-component='error-button'] { + --color-error: var( + --nldesign-component-button-error-background-color, + var(--thematiq-global-color-error) + ); + --color-error-rgb: var( + --nldesign-component-button-error-background-color-rgb, + var(--thematiq-global-color-error-rgb) + ); + --color-error-hover: var( + --nldesign-component-button-error-hover-background-color, + var(--thematiq-global-color-error-hover) + ); +} + +/* success-button */ +.button-vue--success, +.button-vue--vue-success, +[data-thematiq-component='success-button'] { + --color-success: var( + --nldesign-component-button-success-background-color, + var(--thematiq-global-color-success) + ); + --color-element-success: var( + --nldesign-component-button-success-color, + var(--thematiq-global-color-element-success) + ); +} + +/* note-cards */ +.notecard, +[data-thematiq-component='note-cards'] { + --color-info: var( + --nldesign-component-notecard-info-color, + var(--thematiq-global-color-info) + ); + --color-element-info: var( + --nldesign-component-notecard-info-icon-color, + var(--thematiq-global-color-element-info) + ); + --border-radius: var( + --nldesign-component-notecard-border-radius, + var(--thematiq-global-border-radius) + ); + --color-warning: var( + --nldesign-component-notecard-warning-color, + var(--thematiq-global-color-warning) + ); + --color-element-warning: var( + --nldesign-component-notecard-warning-icon-color, + var(--thematiq-global-color-element-warning) + ); + --color-warning-rgb: var( + --nldesign-component-notecard-warning-color-rgb, + var(--thematiq-global-color-warning-rgb) + ); + --color-error: var( + --nldesign-component-notecard-error-color, + var(--thematiq-global-color-error) + ); + --color-element-error: var( + --nldesign-component-notecard-error-icon-color, + var(--thematiq-global-color-element-error) + ); + --color-border-error: var( + --nldesign-component-notecard-error-border-color, + var(--thematiq-global-color-border-error) + ); + --color-error-rgb: var( + --nldesign-component-notecard-error-color-rgb, + var(--thematiq-global-color-error-rgb) + ); + --color-success: var( + --nldesign-component-notecard-success-color, + var(--thematiq-global-color-success) + ); + --color-element-success: var( + --nldesign-component-notecard-success-icon-color, + var(--thematiq-global-color-element-success) + ); + --color-border-success: var( + --nldesign-component-notecard-success-border-color, + var(--thematiq-global-color-border-success) + ); + --color-success-rgb: var( + --nldesign-component-notecard-success-color-rgb, + var(--thematiq-global-color-success-rgb) + ); +} + +/* badge-counter */ +.counter-bubble, +.counter-bubble__counter, +.app-navigation-entry__counter, +[data-thematiq-component='badge-counter'] { + --color-primary-element: var( + --nldesign-component-badge-background-color, + var(--thematiq-global-color-primary-element) + ); + --color-primary-element-text: var( + --nldesign-component-badge-color, + var(--thematiq-global-color-primary-element-text) + ); + --border-radius-pill: var( + --nldesign-component-badge-border-radius, + var(--thematiq-global-border-radius-pill) + ); + --color-favorite: var( + --nldesign-component-badge-favorite-color, + var(--thematiq-global-color-favorite) + ); +} + +/* toast */ +.toastify, +[data-thematiq-component='toast'] { + --color-success: var( + --nldesign-component-toast-success-background-color, + var(--thematiq-global-color-success) + ); + --border-radius: var( + --nldesign-component-toast-border-radius, + var(--thematiq-global-border-radius) + ); + --color-error: var( + --nldesign-component-toast-error-background-color, + var(--thematiq-global-color-error) + ); + --color-warning: var( + --nldesign-component-toast-warning-background-color, + var(--thematiq-global-color-warning) + ); + --color-info: var( + --nldesign-component-toast-info-background-color, + var(--thematiq-global-color-info) + ); +} + +/* heading */ +h1, +h2, +h3, +h4, +h5, +h6, +[data-thematiq-component='heading'] { + --font-face: var( + --nldesign-component-heading-font-family, + var(--thematiq-global-font-face) + ); + --color-main-text: var( + --nldesign-component-heading-color, + var(--thematiq-global-color-main-text) + ); + --font-weight-heading: var( + --nldesign-component-heading-font-weight, + var(--thematiq-global-font-weight-heading) + ); +} + +/* paragraph */ +p, +[data-thematiq-component='paragraph'] { + --color-main-text: var( + --nldesign-component-paragraph-color, + var(--thematiq-global-color-main-text) + ); + --font-face: var( + --nldesign-component-paragraph-font-family, + var(--thematiq-global-font-face) + ); + --font-weight-default: var( + --nldesign-component-paragraph-font-weight, + var(--thematiq-global-font-weight-default) + ); +} + +/* link */ +a, +[data-thematiq-component='link'] { + --color-primary: var( + --nldesign-component-link-color, + var(--thematiq-global-color-primary) + ); + --color-primary-hover: var( + --nldesign-component-link-hover-color, + var(--thematiq-global-color-primary-hover) + ); +} + +/* muted-text */ +.settings-hint, +.app-settings-section__hint, +[data-thematiq-component='muted-text'] { + --color-text-maxcontrast: var( + --nldesign-component-text-muted-color, + var(--thematiq-global-color-text-maxcontrast) + ); + --color-text-light: var( + --nldesign-component-text-light-color, + var(--thematiq-global-color-text-light) + ); + --color-text-lighter: var( + --nldesign-component-text-lighter-color, + var(--thematiq-global-color-text-lighter) + ); +} + +/* status-text */ +.status-text, +.text-error, +.text-warning, +.text-success, +[data-thematiq-component='status-text'] { + --color-text-error: var( + --nldesign-component-text-error-color, + var(--thematiq-global-color-text-error) + ); + --color-text-warning: var( + --nldesign-component-text-warning-color, + var(--thematiq-global-color-text-warning) + ); + --color-text-success: var( + --nldesign-component-text-success-color, + var(--thematiq-global-color-text-success) + ); +} diff --git a/css/primary-lock.css b/css/primary-lock.css new file mode 100644 index 00000000..b843c976 --- /dev/null +++ b/css/primary-lock.css @@ -0,0 +1,67 @@ +/** + * GENERATED FILE — do not edit. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * Source: scripts/mapping/component-tokens.json + * Regenerate: npm run generate:component-scopes + */ + +/* + * The `primary_drives_components` toggle, as a stylesheet. + * + * Emitted by CssInjectionService only while the admin setting is on, and + * emitted AFTER custom-overrides.css so it outranks any per-component value + * already stored there. Nothing is deleted: turning the setting back off + * drops this layer and the stored values take effect again. + * + * Only the tokens flagged `primary` in the mapping appear here — the ones the + * brand primary used to drive before components could be themed separately. + * A component token for a border radius or a font weight is untouched, and + * stays editable while the toggle is on. + */ + +:root { + /* header-bar */ + --nldesign-component-header-background-color: var(--thematiq-global-color-primary) !important; + --nldesign-component-header-color: var(--thematiq-global-color-primary-text) !important; + /* login-button */ + --nldesign-component-login-button-background-color: var(--thematiq-global-color-primary-element) !important; + --nldesign-component-login-button-color: var(--thematiq-global-color-primary-element-text) !important; + --nldesign-component-login-button-hover-background-color: var(--thematiq-global-color-primary-element-hover) !important; + /* logo-slogan */ + --nldesign-component-logo-slogan-color: var(--thematiq-global-color-primary-text) !important; + /* app-navigation */ + --nldesign-component-navigation-active-background-color: var(--thematiq-global-color-primary-element) !important; + /* sidebar */ + --nldesign-component-sidebar-active-color: var(--thematiq-global-color-primary-element) !important; + /* list-item */ + --nldesign-component-list-item-selected-background-color: var(--thematiq-global-color-primary-light) !important; + --nldesign-component-list-item-selected-color: var(--thematiq-global-color-primary-light-text) !important; + --nldesign-component-list-item-selected-hover-background-color: var(--thematiq-global-color-primary-light-hover) !important; + /* breadcrumbs */ + --nldesign-component-breadcrumb-color: var(--thematiq-global-color-primary) !important; + /* text-input */ + --nldesign-component-textbox-focus-border-color: var(--thematiq-global-color-primary-element) !important; + /* checkbox-switch */ + --nldesign-component-checkbox-checked-background-color: var(--thematiq-global-color-primary-element) !important; + /* dialog */ + --nldesign-component-dialog-confirm-background-color: var(--thematiq-global-color-primary-element) !important; + /* progress */ + --nldesign-component-progress-background-color: var(--thematiq-global-color-primary-element) !important; + /* primary-button */ + --nldesign-component-button-primary-action-background-color: var(--thematiq-global-color-primary-element) !important; + --nldesign-component-button-primary-action-color: var(--thematiq-global-color-primary-element-text) !important; + --nldesign-component-button-primary-action-hover-background-color: var(--thematiq-global-color-primary-element-hover) !important; + /* tertiary-button */ + --nldesign-component-button-color: var(--thematiq-global-color-primary-element-light-text) !important; + --nldesign-component-button-hover-background-color: var(--thematiq-global-color-primary-element-light) !important; + --nldesign-component-button-active-background-color: var(--thematiq-global-color-primary-element-light-hover) !important; + /* badge-counter */ + --nldesign-component-badge-background-color: var(--thematiq-global-color-primary-element) !important; + --nldesign-component-badge-color: var(--thematiq-global-color-primary-element-text) !important; + /* link */ + --nldesign-component-link-color: var(--thematiq-global-color-primary) !important; + --nldesign-component-link-hover-color: var(--thematiq-global-color-primary-hover) !important; +} diff --git a/package.json b/package.json index d43fb39c..b777e9b9 100644 --- a/package.json +++ b/package.json @@ -51,7 +51,9 @@ "l10n:build": "node scripts/build-l10n-js.js", "check:l10n-js": "node scripts/build-l10n-js.js --check", "generate:guest-css": "node scripts/generate-guest-css.mjs", - "test:guest-css": "node scripts/generate-guest-css.mjs --check" + "test:guest-css": "node scripts/generate-guest-css.mjs --check", + "generate:component-scopes": "node scripts/generate-component-scopes.mjs", + "test:component-scopes": "node scripts/generate-component-scopes.mjs --check" }, "dependencies": { "@fontsource/fira-sans": "^5.0.0" diff --git a/scripts/generate-component-scopes.mjs b/scripts/generate-component-scopes.mjs new file mode 100644 index 00000000..f8bd8ee7 --- /dev/null +++ b/scripts/generate-component-scopes.mjs @@ -0,0 +1,370 @@ +#!/usr/bin/env node + +/** + * Turn the component-token mapping into the two stylesheets that apply it. + * + * SPDX-License-Identifier: EUPL-1.2 + * SPDX-FileCopyrightText: 2026 Conduction B.V. + * + * WHY THIS EXISTS + * + * Nextcloud has around sixty global variables and every component draws from + * them, so `--color-primary-element` paints the primary button, the selected + * navigation entry, the sidebar's active tab, a focused text input, the checked + * checkbox, the progress bar, the dialog's confirm button and the counter + * bubble. The token editor could only write those globals, which is why moving + * `Primary button` in the playground moved all eight at once. + * + * `scripts/mapping/component-tokens.json` says, for each component, which + * global it consumes and which component token replaces it. This generator + * turns that table into CSS so the mapping is stated once and the stylesheets + * cannot drift from the registry PHP builds out of the same file. + * + * WHAT IT EMITS + * + * css/component-scopes.css the per-component re-scope, always loaded + * css/primary-lock.css the `primary_drives_components` toggle's layer + * + * Neither file declares a component token. They only redirect which value an + * already-existing Nextcloud variable resolves to, so an instance that has + * never set a component token renders byte-identically to one without this + * layer at all. + * + * THE `--thematiq-global-*` CAPTURE IS LOAD-BEARING + * + * A scope rule cannot say + * + * --color-primary-element: var(--X, var(--color-primary-element)) + * + * because a custom property whose value depends on itself is discarded as + * invalid — the same cycle `StockTokensService` documents for + * `--nldesign-color-primary`. So `:root` copies each global under a + * `--thematiq-global-*` name first and the scopes fall back to the copy. The + * copy resolves at `:root`, where the global still holds its ordinary value, + * so there is no cycle and an unset component token is indistinguishable from + * stock. + * + * Note that the capture is deliberately NOT `!important`. custom-overrides.css + * writes admin-set globals at `:root` with `!important` and loads later, so the + * capture has to lose to it: an admin who moves the brand primary must move + * every component that has not been given a value of its own. + * + * WHY THE SCOPES DO NOT NEED `!important` + * + * Custom properties cascade per element. A declaration on `.app-navigation` + * beats one inherited from `:root` whatever its importance, because inheritance + * only supplies a value to an element that has no declaration of its own. So + * these rules win inside their subtree without competing on weight, and + * Nextcloud's own stylesheets keep doing the painting. + */ + +import { readFileSync, writeFileSync, mkdtempSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' +import { tmpdir } from 'os' + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') +const MAPPING_PATH = join(ROOT, 'scripts/mapping/component-tokens.json') +const SCOPES_PATH = join(ROOT, 'css/component-scopes.css') +const LOCK_PATH = join(ROOT, 'css/primary-lock.css') + +const mapping = JSON.parse(readFileSync(MAPPING_PATH, 'utf-8')) + +/** + * The `--thematiq-global-*` name a global is captured under. + * + * @param {string} global A Nextcloud custom property name, with its leading dashes. + * + * @return {string} The capture name. + */ +function captureName(global) { + return mapping.capturePrefix + global.replace(/^--/, '') +} + +/* + * A component that mapped two of its tokens onto the SAME global would emit two + * declarations of one property in one rule, and the second would silently win — + * one of the two chips' controls would do nothing. Caught here rather than in a + * browser. + */ +const collisions = [] +for (const [id, component] of Object.entries(mapping.components)) { + const seen = new Map() + for (const [name, token] of Object.entries(component.tokens)) { + if (seen.has(token.global) === true) { + collisions.push( + id + + ': ' + + seen.get(token.global) + + ' and ' + + name + + ' both re-scope ' + + token.global, + ) + } + seen.set(token.global, name) + } +} +if (collisions.length > 0) { + process.stderr.write( + 'generate-component-scopes: a component maps two tokens onto one global.\n ' + + collisions.join('\n ') + + '\n', + ) + process.exit(1) +} + +// REUSE-IgnoreStart -- the SPDX tags below are DATA, not this file's own: they +// are the header written INTO the two generated stylesheets. Same licence as +// this file, but REUSE should read it off the emitted files, not twice here. +const BANNER = [ + '/**', + ' * GENERATED FILE — do not edit.', + ' *', + ' * SPDX-License-Identifier: EUPL-1.2', + ' * SPDX-FileCopyrightText: 2026 Conduction B.V. ', + ' *', + ' * Source: scripts/mapping/component-tokens.json', + ' * Regenerate: npm run generate:component-scopes', + ' */', + '', +] +// REUSE-IgnoreEnd + +/* ------------------------------------------------------------------ scopes */ + +const scopes = [ + ...BANNER, + '/*', + ' * Component-scoped tokens.', + ' *', + " * Each rule below redeclares, inside one component's subtree, the Nextcloud", + ' * variable that component consumes — so the component follows its own token', + " * when one is set and the brand-wide global when one is not. Nextcloud's own", + ' * stylesheets keep doing the painting; only the value they see changes.', + ' *', + ' * The :root block captures each global under a name the scopes can fall back', + ' * to. Falling back to the global directly would be a self-referential custom', + ' * property, which CSS discards as invalid.', + ' */', + '', + ':root {', +] + +const captured = [] +for (const component of Object.values(mapping.components)) { + for (const token of Object.values(component.tokens)) { + if (captured.includes(token.global) === false) { + captured.push(token.global) + } + } +} +for (const global of captured) { + scopes.push('\t' + captureName(global) + ': var(' + global + ');') +} + +/* + * The playground stage, named after the component it is drawing. + * + * 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 the four + * that are id-scoped -- `#header` and the three under `#body-login` -- because + * a specimen cannot carry an id belonging to the page it is drawn on. Listing + * the stage attribute beside the real selectors lets the specimen resolve the + * same tokens the real component does. js/playground.js sets the attribute. + */ +function specimenSelector(id) { + return "[data-thematiq-component='" + id + "']" +} + +/** + * The `--thematiq-alias-*` name an aliased component token is captured under. + * + * @param {string} target The `--nldesign-component-*` name being redirected. + * + * @return {string} The capture name. + */ +function aliasCaptureName(target) { + return '--thematiq-alias-' + target.replace(/^--/, '') +} + +/* + * Aliases: a component whose own rules read ANOTHER component's tokens. + * + * Re-scoping a Nextcloud global only works where a Nextcloud rule reads it. The + * login button is painted by thematiq itself, from the PRIMARY button's tokens, + * so there is no global left in the chain to redirect — and a more specific + * rule is not an option either: the label is claimed at (1,3,0) by + * `#body-login …:not(…) .button-vue__text` and by its `#content` twin, which + * nothing this layer can write outranks. + * + * So the redirect happens one level up: inside the component, the token its + * rules read IS its own token. The rules that already win then resolve to the + * right value untouched — on the real page and on the playground specimen + * alike, which is what makes the specimen honest without a single + * playground-specific selector in the design-system stylesheet. + * + * Captured for the same reason the globals are: `X: var(Y, var(X))` on one + * element is self-referential, and CSS discards it. + */ +function aliasesOf(component) { + return component.aliases === undefined ? {} : component.aliases +} + +const aliasTargets = [] +for (const component of Object.values(mapping.components)) { + for (const target of Object.keys(aliasesOf(component))) { + if (aliasTargets.includes(target) === false) { + aliasTargets.push(target) + } + } +} + +if (aliasTargets.length > 0) { + scopes.push( + '', + '\t/* Component tokens that a component redirects onto its own. */', + ) + for (const target of aliasTargets) { + scopes.push('\t' + aliasCaptureName(target) + ': var(' + target + ');') + } +} +scopes.push('}', '') + +for (const [id, component] of Object.entries(mapping.components)) { + scopes.push('/* ' + id + ' */') + scopes.push(component.selectors.concat(specimenSelector(id)).join(',\n') + ' {') + for (const [name, token] of Object.entries(component.tokens)) { + scopes.push('\t' + token.global + ': var(') + scopes.push('\t\t' + name + ',') + scopes.push('\t\tvar(' + captureName(token.global) + ')') + scopes.push('\t);') + } + for (const [target, source] of Object.entries(aliasesOf(component))) { + scopes.push('\t' + target + ': var(') + scopes.push('\t\t' + source + ',') + scopes.push('\t\tvar(' + aliasCaptureName(target) + ')') + scopes.push('\t);') + } + scopes.push('}', '') +} + +const scopesOutput = scopes.join('\n') + +/* -------------------------------------------------------------------- lock */ + +const lock = [ + ...BANNER, + '/*', + ' * The `primary_drives_components` toggle, as a stylesheet.', + ' *', + ' * Emitted by CssInjectionService only while the admin setting is on, and', + ' * emitted AFTER custom-overrides.css so it outranks any per-component value', + ' * already stored there. Nothing is deleted: turning the setting back off', + ' * drops this layer and the stored values take effect again.', + ' *', + ' * Only the tokens flagged `primary` in the mapping appear here — the ones the', + ' * brand primary used to drive before components could be themed separately.', + ' * A component token for a border radius or a font weight is untouched, and', + ' * stays editable while the toggle is on.', + ' */', + '', + ':root {', +] + +for (const [id, component] of Object.entries(mapping.components)) { + const locked = Object.entries(component.tokens).filter( + ([, token]) => token.primary === true, + ) + if (locked.length === 0) { + continue + } + lock.push('\t/* ' + id + ' */') + for (const [name, token] of locked) { + lock.push( + '\t' + name + ': var(' + captureName(token.global) + ') !important;', + ) + } +} +lock.push('}', '') + +const lockOutput = lock.join('\n') + +/* ------------------------------------------------------------------- write */ + +const outputs = [ + { path: SCOPES_PATH, label: 'css/component-scopes.css', content: scopesOutput }, + { path: LOCK_PATH, label: 'css/primary-lock.css', content: lockOutput }, +] + +if (process.argv.includes('--check') === false) { + for (const output of outputs) { + writeFileSync(output.path, output.content, 'utf-8') + process.stdout.write( + 'generate-component-scopes: wrote ' + output.label + '\n', + ) + } + process.exit(0) +} + +let stale = false +for (const output of outputs) { + let committed = '' + try { + committed = readFileSync(output.path, 'utf-8') + } catch { + process.stderr.write( + 'generate-component-scopes: ' + output.label + ' is missing.\n', + ) + stale = true + continue + } + + if (committed === output.content) { + continue + } + + stale = true + const temporary = join( + mkdtempSync(join(tmpdir(), 'thematiq-scopes-')), + output.label.replace('css/', ''), + ) + writeFileSync(temporary, output.content, 'utf-8') + + const committedLines = committed.split('\n') + const generatedLines = output.content.split('\n') + for ( + let i = 0; + i < Math.max(committedLines.length, generatedLines.length); + i++ + ) { + if (committedLines[i] === generatedLines[i]) { + continue + } + process.stderr.write( + 'generate-component-scopes: ' + + output.label + + ' is stale.\n' + + ' first difference at line ' + + (i + 1) + + '\n' + + ' committed: ' + + (committedLines[i] ?? '') + + '\n' + + ' generated: ' + + (generatedLines[i] ?? '') + + '\n' + + ' regenerated to ' + + temporary + + '\n', + ) + break + } +} + +if (stale === true) { + process.exit(1) +} + +process.stdout.write('generate-component-scopes: OK — both committed files match.\n') +process.exit(0) diff --git a/scripts/mapping/component-tokens.json b/scripts/mapping/component-tokens.json new file mode 100644 index 00000000..3cfdc58a --- /dev/null +++ b/scripts/mapping/component-tokens.json @@ -0,0 +1,1372 @@ +{ + "$comment": [ + "Component-scoped tokens: the layer that lets one component be restyled without", + "repainting every other component that happens to share a Nextcloud variable.", + "", + "THE PROBLEM THIS FILE SOLVES. Nextcloud has ~60 global variables and every", + "component draws from them, so `--color-primary-element` paints the primary button,", + "the selected navigation entry, the sidebar's active tab, a focused text input, the", + "checked checkbox, the progress bar, the dialog's confirm button and the counter", + "bubble. The token editor could only write those globals, so `Primary button` in the", + "playground moved all eight. A component chip that cannot reach its own component is", + "a control that lies about what it does.", + "", + "THE MECHANISM. Every entry below names a `global` -- the Nextcloud variable the", + "component actually consumes -- and a component token that REPLACES it inside that", + "component's own subtree. scripts/generate-component-scopes.mjs turns this file into", + "css/component-scopes.css, one rule per component:", + "", + " .app-navigation {", + " --color-primary-element: var(", + " --nldesign-component-navigation-active-background-color,", + " var(--thematiq-global-color-primary-element)", + " );", + " }", + "", + "Nextcloud's own stylesheets keep doing the painting; only the value they see inside", + "that subtree changes. Nothing here adds an `!important` element rule, so this layer", + "cannot drift out of step with a Nextcloud release the way a hand-written selector can.", + "", + "WHY THE `--thematiq-global-*` CAPTURE. A scope rule cannot write", + "`--color-primary-element: var(--X, var(--color-primary-element))` -- a custom property", + "whose value depends on itself is discarded as invalid, which is the same cycle", + "StockTokensService documents. So :root copies each global under a `--thematiq-global-*`", + "name first, and the scopes fall back to the copy. The copy is resolved at :root, where", + "the global still has its ordinary value, so there is no cycle and an unset component", + "token is indistinguishable from stock.", + "", + "WHY THE FALLBACK MATTERS. The component tokens are NOT declared here. An admin who has", + "never touched a component leaves them undefined, the `var()` falls through to the", + "captured global, and the instance renders exactly as it did before this layer existed.", + "Only css/systems/nldesign/defaults.css declares them, and only for the nldesign system,", + "which is why the layer is safe to load under `none`, `summer-breeze` and the rest.", + "", + "`primary: true` marks a token the brand primary used to drive. Those are the ones the", + "`primary_drives_components` admin toggle forces back to the brand colour and locks in", + "the editor; see css/primary-lock.css, which is generated from the same flag.", + "", + "`brandTokens` at the foot are the globals themselves. They stay editable from the token", + "editor's four-tab list -- that list IS the brand-level control -- but no chip owns them,", + "because moving one is MEANT to move every component that has not been given a value of", + "its own. tests/vitest/playgroundInventory.spec.js reads the list to keep its coverage", + "guard honest.", + "", + "", + "`aliases` IS THE ESCAPE HATCH, and it has exactly one user so far. Re-scoping a Nextcloud", + "global only works where a NEXTCLOUD rule reads it. The login button is painted by thematiq", + "itself, from the PRIMARY button's tokens, so there is no global left in the chain to redirect.", + "Writing a more specific rule does not work either: its label is claimed at (1,3,0) by", + "`#body-login …:not(…) .button-vue__text` and by the `#content` twin that catches the playground", + "specimen, and nothing this layer can write outranks those.", + "", + "So an alias redirects one level up: inside the component, the token its own rules read IS its", + "own token. `--nldesign-component-button-primary-action-background-color` becomes", + "`--nldesign-component-login-button-background-color` within the login button's selectors. The", + "rules that already win then resolve to the right value untouched — on the real login page and", + "on the playground specimen alike, which is what lets the specimen be honest without a single", + "playground-specific selector in the design-system stylesheets.", + "BOTH RUNTIMES READ THIS FILE. lib/Service/TokenRegistry.php builds the editable registry", + "from it and js/playground/components.json names the same tokens, so a token cannot exist", + "in one and be missing from the other." + ], + "version": 1, + "capturePrefix": "--thematiq-global-", + "componentPrefix": "--nldesign-component-", + "components": { + "header-bar": { + "tab": "login", + "selectors": ["#header"], + "tokens": { + "--nldesign-component-header-background-color": { + "global": "--color-primary", + "label": "Header background", + "type": "color", + "primary": true, + "paints": "The header, when the set declares no header colour", + "callout": 1 + }, + "--nldesign-component-header-color": { + "global": "--color-primary-text", + "label": "Header glyphs", + "type": "color", + "primary": true, + "paints": "App icons, search and the account glyphs", + "callout": 1 + }, + "--nldesign-component-header-hover-background-color": { + "global": "--color-background-hover", + "label": "Header hovered entry", + "type": "color", + "primary": false, + "paints": "Hovered app entry", + "callout": 2 + } + } + }, + "login-card": { + "tab": "login", + "selectors": ["#body-login .guest-box", "#body-login .login-box"], + "tokens": { + "--nldesign-component-login-box-border-radius": { + "global": "--border-radius-large", + "label": "Login card corner", + "type": "text", + "primary": false, + "paints": "Its corner", + "callout": 1 + }, + "--nldesign-component-login-box-border-color": { + "global": "--color-border", + "label": "Login card edge", + "type": "color", + "primary": false, + "paints": "Its border", + "callout": 1 + } + } + }, + "login-button": { + "tab": "login", + "selectors": [ + "#body-login .button-vue--primary", + "#body-login .button-vue--vue-primary", + "#body-login input[type='submit']", + "#body-login button.primary", + ".login-form button[type='submit']" + ], + "tokens": { + "--nldesign-component-login-button-background-color": { + "global": "--color-primary-element", + "label": "Login button background", + "type": "color", + "primary": true, + "paints": "Background", + "callout": 1 + }, + "--nldesign-component-login-button-color": { + "global": "--color-primary-element-text", + "label": "Login button label", + "type": "color", + "primary": true, + "paints": "Label", + "callout": 1 + }, + "--nldesign-component-login-button-hover-background-color": { + "global": "--color-primary-element-hover", + "label": "Login button hover background", + "type": "color", + "primary": true, + "paints": "Background on hover", + "callout": 2 + }, + "--nldesign-component-login-button-border-radius": { + "global": "--border-radius-element", + "label": "Login button corner", + "type": "text", + "primary": false, + "paints": "Corner", + "callout": 1 + } + }, + "aliases": { + "--nldesign-component-button-primary-action-background-color": "--nldesign-component-login-button-background-color", + "--nldesign-component-button-primary-action-border-color": "--nldesign-component-login-button-background-color", + "--nldesign-component-button-primary-action-color": "--nldesign-component-login-button-color", + "--nldesign-component-button-primary-action-hover-background-color": "--nldesign-component-login-button-hover-background-color", + "--nldesign-component-button-border-radius": "--nldesign-component-login-button-border-radius" + } + }, + "logo-slogan": { + "tab": "login", + "selectors": ["#body-login .header-guest", "#body-login .logo"], + "tokens": { + "--nldesign-component-logo-slogan-color": { + "global": "--color-primary-text", + "label": "Slogan text", + "type": "color", + "primary": true, + "paints": "Slogan text", + "callout": 1 + }, + "--nldesign-component-logo-slogan-font-family": { + "global": "--font-face", + "label": "Slogan typeface", + "type": "text", + "primary": false, + "paints": "Slogan typeface", + "callout": 1 + } + } + }, + "app-navigation": { + "tab": "content", + "selectors": [ + "#app-navigation", + "#app-navigation-vue", + ".app-navigation" + ], + "tokens": { + "--nldesign-component-navigation-border-radius": { + "global": "--border-radius-element", + "label": "Navigation entry corner", + "type": "text", + "primary": false, + "paints": "Entry corners", + "callout": 1 + }, + "--nldesign-component-navigation-color": { + "global": "--color-main-text", + "label": "Navigation label", + "type": "color", + "primary": false, + "paints": "Entry labels and icons, the selected one included", + "callout": 1 + }, + "--nldesign-component-navigation-active-background-color": { + "global": "--color-primary-element", + "label": "Navigation selected entry", + "type": "color", + "primary": true, + "paints": "The selected entry's wash and its marker stripe", + "callout": 2 + } + } + }, + "content-card": { + "tab": "content", + "selectors": ["#app-content", ".app-content", "#content"], + "tokens": { + "--nldesign-component-content-card-container-border-radius": { + "global": "--body-container-radius", + "label": "Content container corner", + "type": "text", + "primary": false, + "paints": "Container corner", + "callout": 1 + }, + "--nldesign-component-content-card-border-radius": { + "global": "--border-radius-rounded", + "label": "Content rounded surfaces", + "type": "text", + "primary": false, + "paints": "Rounded surfaces inside it", + "callout": 1 + }, + "--nldesign-component-content-card-border-color": { + "global": "--color-border", + "label": "Content rule", + "type": "color", + "primary": false, + "paints": "The rule between list and detail", + "callout": 1 + }, + "--nldesign-component-content-card-scrollbar-color": { + "global": "--color-scrollbar", + "label": "Content scrollbar", + "type": "color", + "primary": false, + "paints": "Scrollbar thumb", + "callout": 1 + } + } + }, + "table": { + "tab": "content", + "selectors": ["table"], + "tokens": { + "--nldesign-component-table-header-color": { + "global": "--color-text-maxcontrast", + "label": "Table header label", + "type": "color", + "primary": false, + "paints": "Header row labels and secondary cells", + "callout": 1 + }, + "--nldesign-component-table-border-color": { + "global": "--color-border", + "label": "Table row rule", + "type": "color", + "primary": false, + "paints": "The rule under each row", + "callout": 1 + }, + "--nldesign-component-table-zebra-background-color": { + "global": "--color-background-dark", + "label": "Table zebra row", + "type": "color", + "primary": false, + "paints": "Zebra row", + "callout": 1 + }, + "--nldesign-component-table-row-hover-background-color": { + "global": "--color-background-hover", + "label": "Table hovered row", + "type": "color", + "primary": false, + "paints": "Hovered row", + "callout": 2 + } + } + }, + "sidebar": { + "tab": "content", + "selectors": ["#app-sidebar", "#app-sidebar-vue", ".app-sidebar"], + "tokens": { + "--nldesign-component-sidebar-border-color": { + "global": "--color-border", + "label": "Sidebar edge", + "type": "color", + "primary": false, + "paints": "Its left edge and the tab strip rule", + "callout": 1 + }, + "--nldesign-component-sidebar-divider-color": { + "global": "--color-border-dark", + "label": "Sidebar divider", + "type": "color", + "primary": false, + "paints": "The stronger divider", + "callout": 1 + }, + "--nldesign-component-sidebar-border-radius": { + "global": "--border-radius-large", + "label": "Sidebar corner", + "type": "text", + "primary": false, + "paints": "Panel corner", + "callout": 1 + }, + "--nldesign-component-sidebar-active-color": { + "global": "--color-primary-element", + "label": "Sidebar active tab", + "type": "color", + "primary": true, + "paints": "The active tab underline", + "callout": 2 + } + } + }, + "list-item": { + "tab": "content", + "selectors": [".list-item", ".list-item__wrapper"], + "tokens": { + "--nldesign-component-list-item-border-radius": { + "global": "--border-radius-element", + "label": "List row corner", + "type": "text", + "primary": false, + "paints": "Row corner", + "callout": 1 + }, + "--nldesign-component-list-item-subline-color": { + "global": "--color-text-maxcontrast", + "label": "List row subline", + "type": "color", + "primary": false, + "paints": "The subline", + "callout": 1 + }, + "--nldesign-component-list-item-hover-background-color": { + "global": "--color-background-hover", + "label": "List row hover", + "type": "color", + "primary": false, + "paints": "Hovered row", + "callout": 2 + }, + "--nldesign-component-list-item-transition-duration": { + "global": "--animation-quick", + "label": "List row transition", + "type": "text", + "primary": false, + "paints": "The hover transition", + "callout": 2 + }, + "--nldesign-component-list-item-selected-background-color": { + "global": "--color-primary-light", + "label": "List row selected", + "type": "color", + "primary": true, + "paints": "Selected row", + "callout": 3 + }, + "--nldesign-component-list-item-selected-color": { + "global": "--color-primary-light-text", + "label": "List row selected text", + "type": "color", + "primary": true, + "paints": "Its text", + "callout": 3 + }, + "--nldesign-component-list-item-selected-hover-background-color": { + "global": "--color-primary-light-hover", + "label": "List row selected hover", + "type": "color", + "primary": true, + "paints": "The selected row on hover", + "callout": 3 + } + } + }, + "empty-content": { + "tab": "content", + "selectors": [".empty-content"], + "tokens": { + "--nldesign-component-empty-content-color": { + "global": "--color-main-text", + "label": "Empty state title", + "type": "color", + "primary": false, + "paints": "Its title", + "callout": 1 + }, + "--nldesign-component-empty-content-description-color": { + "global": "--color-text-maxcontrast", + "label": "Empty state description", + "type": "color", + "primary": false, + "paints": "Its description", + "callout": 1 + }, + "--nldesign-component-empty-content-icon-background-color": { + "global": "--color-background-dark", + "label": "Empty state icon disc", + "type": "color", + "primary": false, + "paints": "The disc behind the icon", + "callout": 1 + } + } + }, + "actions-menu": { + "tab": "content", + "selectors": [".popover", ".popovermenu", ".v-popper__popper"], + "tokens": { + "--nldesign-component-actions-menu-color": { + "global": "--color-main-text", + "label": "Menu entry label", + "type": "color", + "primary": false, + "paints": "Entry labels", + "callout": 1 + }, + "--nldesign-component-actions-menu-border-radius": { + "global": "--border-radius-large", + "label": "Menu corner", + "type": "text", + "primary": false, + "paints": "Panel corner", + "callout": 1 + }, + "--nldesign-component-actions-menu-hover-background-color": { + "global": "--color-background-hover", + "label": "Menu hovered entry", + "type": "color", + "primary": false, + "paints": "Hovered entry", + "callout": 2 + } + } + }, + "avatar": { + "tab": "content", + "selectors": [".avatardiv", ".user-status-icon"], + "tokens": { + "--nldesign-component-avatar-online-status-color": { + "global": "--color-success", + "label": "Avatar online dot", + "type": "color", + "primary": false, + "paints": "The online status dot", + "callout": 1 + } + } + }, + "breadcrumbs": { + "tab": "content", + "selectors": [".breadcrumb", ".vue-crumb"], + "tokens": { + "--nldesign-component-breadcrumb-color": { + "global": "--color-primary", + "label": "Breadcrumb link", + "type": "color", + "primary": true, + "paints": "A crumb that is a link", + "callout": 1 + }, + "--nldesign-component-breadcrumb-separator-color": { + "global": "--color-text-maxcontrast", + "label": "Breadcrumb separator", + "type": "color", + "primary": false, + "paints": "The separators", + "callout": 1 + }, + "--nldesign-component-breadcrumb-hover-background-color": { + "global": "--color-background-hover", + "label": "Breadcrumb hover", + "type": "color", + "primary": false, + "paints": "The hovered crumb", + "callout": 2 + } + } + }, + "settings-section": { + "tab": "content", + "selectors": [".settings-section", ".app-settings-section"], + "tokens": { + "--nldesign-component-settings-section-heading-color": { + "global": "--color-main-text", + "label": "Settings heading", + "type": "color", + "primary": false, + "paints": "The section heading", + "callout": 1 + }, + "--nldesign-component-settings-section-hint-color": { + "global": "--color-text-maxcontrast", + "label": "Settings hint", + "type": "color", + "primary": false, + "paints": "Its hint text", + "callout": 1 + }, + "--nldesign-component-settings-section-border-color": { + "global": "--color-border", + "label": "Settings rule", + "type": "color", + "primary": false, + "paints": "The rule between sections", + "callout": 1 + } + } + }, + "text-input": { + "tab": "content", + "selectors": [ + ".input-field", + "input[type='text']", + "input[type='password']", + "input[type='email']", + "input[type='search']" + ], + "tokens": { + "--nldesign-component-textbox-border-color": { + "global": "--color-border-maxcontrast", + "label": "Text input border", + "type": "color", + "primary": false, + "paints": "Border", + "callout": 1 + }, + "--nldesign-component-textbox-placeholder-color": { + "global": "--color-placeholder-dark", + "label": "Text input placeholder", + "type": "color", + "primary": false, + "paints": "Placeholder", + "callout": 1 + }, + "--nldesign-component-textbox-placeholder-light-color": { + "global": "--color-placeholder-light", + "label": "Text input placeholder (tinted)", + "type": "color", + "primary": false, + "paints": "Placeholder on a tinted field", + "callout": 1 + }, + "--nldesign-component-textbox-border-radius": { + "global": "--border-radius-element", + "label": "Text input corner", + "type": "text", + "primary": false, + "paints": "Corner", + "callout": 1 + }, + "--nldesign-component-textbox-focus-border-color": { + "global": "--color-primary-element", + "label": "Text input focus", + "type": "color", + "primary": true, + "paints": "Focused border and label", + "callout": 2 + }, + "--nldesign-component-textbox-invalid-border-color": { + "global": "--color-error", + "label": "Text input invalid", + "type": "color", + "primary": false, + "paints": "Invalid border", + "callout": 3 + } + } + }, + "select": { + "tab": "content", + "selectors": ["select", ".v-select"], + "tokens": { + "--nldesign-component-form-select-border-color": { + "global": "--color-border-maxcontrast", + "label": "Select border", + "type": "color", + "primary": false, + "paints": "Control border", + "callout": 1 + }, + "--nldesign-component-form-select-border-radius": { + "global": "--border-radius-element", + "label": "Select corner", + "type": "text", + "primary": false, + "paints": "Corner", + "callout": 1 + }, + "--nldesign-component-form-select-option-hover-background-color": { + "global": "--color-background-hover", + "label": "Select hovered option", + "type": "color", + "primary": false, + "paints": "Hovered option", + "callout": 2 + } + } + }, + "checkbox-switch": { + "tab": "content", + "selectors": [ + ".checkbox-radio-switch", + "input[type='checkbox']", + "input[type='radio']" + ], + "tokens": { + "--nldesign-component-checkbox-border-color": { + "global": "--color-border-maxcontrast", + "label": "Checkbox outline", + "type": "color", + "primary": false, + "paints": "The unchecked outline", + "callout": 1 + }, + "--nldesign-component-checkbox-border-radius": { + "global": "--border-radius-small", + "label": "Checkbox corner", + "type": "text", + "primary": false, + "paints": "The checkbox corner", + "callout": 1 + }, + "--nldesign-component-checkbox-checked-background-color": { + "global": "--color-primary-element", + "label": "Checkbox checked", + "type": "color", + "primary": true, + "paints": "The checked mark, dot and switch track", + "callout": 2 + } + } + }, + "textarea": { + "tab": "content", + "selectors": ["textarea"], + "tokens": { + "--nldesign-component-textarea-border-color": { + "global": "--color-border-maxcontrast", + "label": "Textarea border", + "type": "color", + "primary": false, + "paints": "Border", + "callout": 1 + }, + "--nldesign-component-textarea-border-radius": { + "global": "--border-radius", + "label": "Textarea corner", + "type": "text", + "primary": false, + "paints": "Corner", + "callout": 1 + } + }, + "aliases": { + "--nldesign-component-textbox-border-color": "--nldesign-component-textarea-border-color", + "--nldesign-component-textbox-border-radius": "--nldesign-component-textarea-border-radius" + } + }, + "dialog": { + "tab": "content", + "selectors": [".modal-container", ".modal-wrapper", ".dialog"], + "tokens": { + "--nldesign-component-dialog-border-radius": { + "global": "--border-radius-large", + "label": "Dialog corner", + "type": "text", + "primary": false, + "paints": "Corner", + "callout": 1 + }, + "--nldesign-component-dialog-border-color": { + "global": "--color-border", + "label": "Dialog header rule", + "type": "color", + "primary": false, + "paints": "The rule under the header", + "callout": 1 + }, + "--nldesign-component-dialog-transition-duration": { + "global": "--animation-slow", + "label": "Dialog transition", + "type": "text", + "primary": false, + "paints": "Its open and close transition", + "callout": 1 + }, + "--nldesign-component-dialog-confirm-background-color": { + "global": "--color-primary-element", + "label": "Dialog confirm button", + "type": "color", + "primary": true, + "paints": "Its confirm button", + "callout": 2 + } + } + }, + "progress": { + "tab": "content", + "selectors": ["progress", ".progress-bar", ".icon-loading"], + "tokens": { + "--nldesign-component-progress-background-color": { + "global": "--color-primary-element", + "label": "Progress fill", + "type": "color", + "primary": true, + "paints": "The filled part of the bar", + "callout": 1 + }, + "--nldesign-component-progress-track-background-color": { + "global": "--color-background-darker", + "label": "Progress track", + "type": "color", + "primary": false, + "paints": "Its track", + "callout": 1 + }, + "--nldesign-component-progress-border-radius": { + "global": "--border-radius-pill", + "label": "Progress shape", + "type": "text", + "primary": false, + "paints": "Bar shape", + "callout": 1 + }, + "--nldesign-component-progress-transition-duration": { + "global": "--animation-slow", + "label": "Spinner rotation", + "type": "text", + "primary": false, + "paints": "The spinner's rotation", + "callout": 2 + } + } + }, + "primary-button": { + "tab": "status", + "selectors": [ + ".button-vue--primary", + ".button-vue--vue-primary", + "button.primary", + "input[type='submit']" + ], + "tokens": { + "--nldesign-component-button-primary-action-background-color": { + "global": "--color-primary-element", + "label": "Primary button background", + "type": "color", + "primary": true, + "paints": "Background", + "callout": 1 + }, + "--nldesign-component-button-primary-action-color": { + "global": "--color-primary-element-text", + "label": "Primary button label", + "type": "color", + "primary": true, + "paints": "Label", + "callout": 1 + }, + "--nldesign-component-button-primary-action-hover-background-color": { + "global": "--color-primary-element-hover", + "label": "Primary button hover", + "type": "color", + "primary": true, + "paints": "Background on hover", + "callout": 2 + }, + "--nldesign-component-button-focus-border-color": { + "global": "--color-main-text", + "label": "Button focus ring", + "type": "color", + "primary": false, + "paints": "Focus ring", + "callout": 3 + }, + "--nldesign-component-button-border-radius": { + "global": "--border-radius-element", + "label": "Button corner", + "type": "text", + "primary": false, + "paints": "Corner", + "callout": 1 + }, + "--nldesign-component-button-font-weight": { + "global": "--font-weight-element", + "label": "Button label weight", + "type": "text", + "primary": false, + "paints": "Label weight", + "callout": 1 + } + }, + "aliases": { + "--nldesign-component-button-primary-action-font-weight": "--nldesign-component-button-font-weight" + } + }, + "secondary-button": { + "tab": "status", + "selectors": [".button-vue--secondary", ".button-vue--vue-secondary"], + "tokens": { + "--nldesign-component-button-secondary-action-background-color": { + "global": "--color-background-dark", + "label": "Secondary button background", + "type": "color", + "primary": false, + "paints": "Background", + "callout": 1 + }, + "--nldesign-component-button-secondary-action-color": { + "global": "--color-main-text", + "label": "Secondary button label", + "type": "color", + "primary": false, + "paints": "Label", + "callout": 1 + }, + "--nldesign-component-button-secondary-action-hover-background-color": { + "global": "--color-background-darker", + "label": "Secondary button hover", + "type": "color", + "primary": false, + "paints": "Background on hover", + "callout": 2 + }, + "--nldesign-component-button-secondary-action-border-radius": { + "global": "--border-radius-element", + "label": "Secondary button corner", + "type": "text", + "primary": false, + "paints": "Corner", + "callout": 1 + } + }, + "aliases": { + "--nldesign-component-button-border-radius": "--nldesign-component-button-secondary-action-border-radius" + } + }, + "tertiary-button": { + "tab": "status", + "selectors": [ + ".button-vue--tertiary", + ".button-vue--vue-tertiary", + ".button-vue--tertiary-no-background" + ], + "tokens": { + "--nldesign-component-button-color": { + "global": "--color-primary-element-light-text", + "label": "Tertiary button label", + "type": "color", + "primary": true, + "paints": "Label", + "callout": 1 + }, + "--nldesign-component-button-hover-background-color": { + "global": "--color-primary-element-light", + "label": "Tertiary button hover", + "type": "color", + "primary": true, + "paints": "Background on hover", + "callout": 2 + }, + "--nldesign-component-button-active-background-color": { + "global": "--color-primary-element-light-hover", + "label": "Tertiary button pressed", + "type": "color", + "primary": true, + "paints": "Background when pressed", + "callout": 3 + } + } + }, + "error-button": { + "tab": "status", + "selectors": [".button-vue--error", ".button-vue--vue-error"], + "tokens": { + "--nldesign-component-button-error-background-color": { + "global": "--color-error", + "label": "Error button background", + "type": "color", + "primary": false, + "paints": "Background", + "callout": 1 + }, + "--nldesign-component-button-error-background-color-rgb": { + "global": "--color-error-rgb", + "label": "Error button fill (RGB)", + "type": "text", + "primary": false, + "paints": "Its tinted fill, mixed from the same colour", + "callout": 1 + }, + "--nldesign-component-button-error-hover-background-color": { + "global": "--color-error-hover", + "label": "Error button hover", + "type": "color", + "primary": false, + "paints": "Background on hover", + "callout": 2 + } + } + }, + "success-button": { + "tab": "status", + "selectors": [".button-vue--success", ".button-vue--vue-success"], + "tokens": { + "--nldesign-component-button-success-background-color": { + "global": "--color-success", + "label": "Success button background", + "type": "color", + "primary": false, + "paints": "Background", + "callout": 1 + }, + "--nldesign-component-button-success-color": { + "global": "--color-element-success", + "label": "Success button icon", + "type": "color", + "primary": false, + "paints": "Its icon", + "callout": 1 + } + } + }, + "note-cards": { + "tab": "status", + "selectors": [".notecard"], + "tokens": { + "--nldesign-component-notecard-info-color": { + "global": "--color-info", + "label": "Note card info stripe", + "type": "color", + "primary": false, + "paints": "Info stripe", + "callout": 1 + }, + "--nldesign-component-notecard-info-icon-color": { + "global": "--color-element-info", + "label": "Note card info glyph", + "type": "color", + "primary": false, + "paints": "Info glyph", + "callout": 1 + }, + "--nldesign-component-notecard-border-radius": { + "global": "--border-radius", + "label": "Note card corner", + "type": "text", + "primary": false, + "paints": "Card corner", + "callout": 1 + }, + "--nldesign-component-notecard-warning-color": { + "global": "--color-warning", + "label": "Note card warning stripe", + "type": "color", + "primary": false, + "paints": "Warning stripe", + "callout": 2 + }, + "--nldesign-component-notecard-warning-icon-color": { + "global": "--color-element-warning", + "label": "Note card warning glyph", + "type": "color", + "primary": false, + "paints": "Warning glyph", + "callout": 2 + }, + "--nldesign-component-notecard-warning-color-rgb": { + "global": "--color-warning-rgb", + "label": "Note card warning fill (RGB)", + "type": "text", + "primary": false, + "paints": "Warning fill, mixed from the same colour", + "callout": 2 + }, + "--nldesign-component-notecard-error-color": { + "global": "--color-error", + "label": "Note card error stripe", + "type": "color", + "primary": false, + "paints": "Error stripe", + "callout": 3 + }, + "--nldesign-component-notecard-error-icon-color": { + "global": "--color-element-error", + "label": "Note card error glyph", + "type": "color", + "primary": false, + "paints": "Error glyph", + "callout": 3 + }, + "--nldesign-component-notecard-error-border-color": { + "global": "--color-border-error", + "label": "Note card error border", + "type": "color", + "primary": false, + "paints": "Error card border", + "callout": 3 + }, + "--nldesign-component-notecard-error-color-rgb": { + "global": "--color-error-rgb", + "label": "Note card error fill (RGB)", + "type": "text", + "primary": false, + "paints": "Error fill, mixed from the same colour", + "callout": 3 + }, + "--nldesign-component-notecard-success-color": { + "global": "--color-success", + "label": "Note card success stripe", + "type": "color", + "primary": false, + "paints": "Success stripe", + "callout": 4 + }, + "--nldesign-component-notecard-success-icon-color": { + "global": "--color-element-success", + "label": "Note card success glyph", + "type": "color", + "primary": false, + "paints": "Success glyph", + "callout": 4 + }, + "--nldesign-component-notecard-success-border-color": { + "global": "--color-border-success", + "label": "Note card success border", + "type": "color", + "primary": false, + "paints": "Success card border", + "callout": 4 + }, + "--nldesign-component-notecard-success-color-rgb": { + "global": "--color-success-rgb", + "label": "Note card success fill (RGB)", + "type": "text", + "primary": false, + "paints": "Success fill, mixed from the same colour", + "callout": 4 + } + } + }, + "badge-counter": { + "tab": "status", + "selectors": [ + ".counter-bubble", + ".counter-bubble__counter", + ".app-navigation-entry__counter" + ], + "tokens": { + "--nldesign-component-badge-background-color": { + "global": "--color-primary-element", + "label": "Badge background", + "type": "color", + "primary": true, + "paints": "The highlighted bubble", + "callout": 1 + }, + "--nldesign-component-badge-color": { + "global": "--color-primary-element-text", + "label": "Badge number", + "type": "color", + "primary": true, + "paints": "Its number", + "callout": 1 + }, + "--nldesign-component-badge-border-radius": { + "global": "--border-radius-pill", + "label": "Badge shape", + "type": "text", + "primary": false, + "paints": "Bubble shape", + "callout": 1 + }, + "--nldesign-component-badge-favorite-color": { + "global": "--color-favorite", + "label": "Favourite star", + "type": "color", + "primary": false, + "paints": "The favourite star", + "callout": 2 + } + } + }, + "toast": { + "tab": "status", + "selectors": [".toastify"], + "tokens": { + "--nldesign-component-toast-success-background-color": { + "global": "--color-success", + "label": "Toast success", + "type": "color", + "primary": false, + "paints": "Success toast", + "callout": 1 + }, + "--nldesign-component-toast-border-radius": { + "global": "--border-radius", + "label": "Toast corner", + "type": "text", + "primary": false, + "paints": "Corner", + "callout": 1 + }, + "--nldesign-component-toast-error-background-color": { + "global": "--color-error", + "label": "Toast error", + "type": "color", + "primary": false, + "paints": "Error toast", + "callout": 2 + }, + "--nldesign-component-toast-warning-background-color": { + "global": "--color-warning", + "label": "Toast warning", + "type": "color", + "primary": false, + "paints": "Warning toast", + "callout": 3 + }, + "--nldesign-component-toast-info-background-color": { + "global": "--color-info", + "label": "Toast info", + "type": "color", + "primary": false, + "paints": "Info toast", + "callout": 4 + } + } + }, + "heading": { + "tab": "typography", + "selectors": ["h1", "h2", "h3", "h4", "h5", "h6"], + "tokens": { + "--nldesign-component-heading-font-family": { + "global": "--font-face", + "label": "Heading typeface", + "type": "text", + "primary": false, + "paints": "Typeface", + "callout": 1 + }, + "--nldesign-component-heading-color": { + "global": "--color-main-text", + "label": "Heading colour", + "type": "color", + "primary": false, + "paints": "Heading colour", + "callout": 1 + }, + "--nldesign-component-heading-font-weight": { + "global": "--font-weight-heading", + "label": "Heading weight", + "type": "text", + "primary": false, + "paints": "Heading weight", + "callout": 1 + } + } + }, + "paragraph": { + "tab": "typography", + "selectors": ["p"], + "tokens": { + "--nldesign-component-paragraph-color": { + "global": "--color-main-text", + "label": "Body text colour", + "type": "color", + "primary": false, + "paints": "Body text", + "callout": 1 + }, + "--nldesign-component-paragraph-font-family": { + "global": "--font-face", + "label": "Body typeface", + "type": "text", + "primary": false, + "paints": "Typeface", + "callout": 1 + }, + "--nldesign-component-paragraph-font-weight": { + "global": "--font-weight-default", + "label": "Body weight", + "type": "text", + "primary": false, + "paints": "Body weight", + "callout": 1 + } + } + }, + "link": { + "tab": "typography", + "selectors": ["a"], + "tokens": { + "--nldesign-component-link-color": { + "global": "--color-primary", + "label": "Link colour", + "type": "color", + "primary": true, + "paints": "Link colour", + "callout": 1 + }, + "--nldesign-component-link-hover-color": { + "global": "--color-primary-hover", + "label": "Link hover colour", + "type": "color", + "primary": true, + "paints": "Link on hover", + "callout": 2 + } + } + }, + "muted-text": { + "tab": "typography", + "selectors": [".settings-hint", ".app-settings-section__hint"], + "tokens": { + "--nldesign-component-text-muted-color": { + "global": "--color-text-maxcontrast", + "label": "Muted text", + "type": "color", + "primary": false, + "paints": "Secondary text that still has to pass AA", + "callout": 1 + }, + "--nldesign-component-text-light-color": { + "global": "--color-text-light", + "label": "Light text", + "type": "color", + "primary": false, + "paints": "Text one step lighter than body", + "callout": 1 + }, + "--nldesign-component-text-lighter-color": { + "global": "--color-text-lighter", + "label": "Lighter text", + "type": "color", + "primary": false, + "paints": "The lightest text Nextcloud uses", + "callout": 1 + } + } + }, + "status-text": { + "tab": "typography", + "selectors": [ + ".status-text", + ".text-error", + ".text-warning", + ".text-success" + ], + "tokens": { + "--nldesign-component-text-error-color": { + "global": "--color-text-error", + "label": "Error text", + "type": "color", + "primary": false, + "paints": "Error text", + "callout": 1 + }, + "--nldesign-component-text-warning-color": { + "global": "--color-text-warning", + "label": "Warning text", + "type": "color", + "primary": false, + "paints": "Warning text", + "callout": 2 + }, + "--nldesign-component-text-success-color": { + "global": "--color-text-success", + "label": "Success text", + "type": "color", + "primary": false, + "paints": "Success text", + "callout": 3 + } + } + } + }, + "brandTokens": [ + "--color-primary", + "--color-primary-text", + "--color-background-hover", + "--border-radius-large", + "--color-border", + "--color-primary-element", + "--color-primary-element-text", + "--color-primary-element-hover", + "--border-radius-element", + "--font-face", + "--color-main-text", + "--body-container-radius", + "--border-radius-rounded", + "--color-scrollbar", + "--color-text-maxcontrast", + "--color-background-dark", + "--color-border-dark", + "--animation-quick", + "--color-primary-light", + "--color-primary-light-text", + "--color-primary-light-hover", + "--color-success", + "--color-border-maxcontrast", + "--color-placeholder-dark", + "--color-placeholder-light", + "--color-error", + "--border-radius-small", + "--border-radius", + "--animation-slow", + "--color-background-darker", + "--border-radius-pill", + "--font-weight-element", + "--color-primary-element-light-text", + "--color-primary-element-light", + "--color-primary-element-light-hover", + "--color-error-rgb", + "--color-error-hover", + "--color-element-success", + "--color-info", + "--color-element-info", + "--color-warning", + "--color-element-warning", + "--color-warning-rgb", + "--color-element-error", + "--color-border-error", + "--color-border-success", + "--color-success-rgb", + "--color-favorite", + "--font-weight-heading", + "--font-weight-default", + "--color-primary-hover", + "--color-text-light", + "--color-text-lighter", + "--color-text-error", + "--color-text-warning", + "--color-text-success" + ] +} From f4028a073fca8f2935e5edf8df999414ad175298 Mon Sep 17 00:00:00 2001 From: Remko Date: Wed, 23 Sep 2026 13:23:11 +0200 Subject: [PATCH 09/72] feat(tokens): paint the nldesign layers from the component tokens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- css/systems/nldesign/defaults.css | 36 +++-- css/systems/nldesign/element-overrides.css | 116 ++++++++++++--- css/systems/nldesign/theme.css | 163 ++++++++++++++++++--- 3 files changed, 262 insertions(+), 53 deletions(-) diff --git a/css/systems/nldesign/defaults.css b/css/systems/nldesign/defaults.css index 718e91b1..90bcd0a4 100644 --- a/css/systems/nldesign/defaults.css +++ b/css/systems/nldesign/defaults.css @@ -318,41 +318,51 @@ * COMPONENT TOKENS — Typography: Headings * =================================================================== */ + /* ALL headings, which is the level the playground's `Headings` chip edits. + * The six per-level tokens below derive from these three rather than from + * the brand tokens directly, so moving the heading colour or weight once + * moves h1 through h6 — 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. */ + --nldesign-component-heading-font-family: var(--nldesign-font-family); + --nldesign-component-heading-font-weight: var(--nldesign-font-weight-heading); + --nldesign-component-heading-color: var(--nldesign-color-text); + /* Heading 1 */ --nldesign-component-heading-1-font-size: 2.5rem; - --nldesign-component-heading-1-font-weight: var(--nldesign-font-weight-heading); + --nldesign-component-heading-1-font-weight: var(--nldesign-component-heading-font-weight); --nldesign-component-heading-1-line-height: 1.2; - --nldesign-component-heading-1-color: var(--nldesign-color-text); + --nldesign-component-heading-1-color: var(--nldesign-component-heading-color); /* Heading 2 */ --nldesign-component-heading-2-font-size: 2rem; - --nldesign-component-heading-2-font-weight: var(--nldesign-font-weight-heading); + --nldesign-component-heading-2-font-weight: var(--nldesign-component-heading-font-weight); --nldesign-component-heading-2-line-height: 1.25; - --nldesign-component-heading-2-color: var(--nldesign-color-text); + --nldesign-component-heading-2-color: var(--nldesign-component-heading-color); /* Heading 3 */ --nldesign-component-heading-3-font-size: 1.5rem; - --nldesign-component-heading-3-font-weight: var(--nldesign-font-weight-heading); + --nldesign-component-heading-3-font-weight: var(--nldesign-component-heading-font-weight); --nldesign-component-heading-3-line-height: 1.3; - --nldesign-component-heading-3-color: var(--nldesign-color-text); + --nldesign-component-heading-3-color: var(--nldesign-component-heading-color); /* Heading 4 */ --nldesign-component-heading-4-font-size: 1.25rem; - --nldesign-component-heading-4-font-weight: var(--nldesign-font-weight-heading); + --nldesign-component-heading-4-font-weight: var(--nldesign-component-heading-font-weight); --nldesign-component-heading-4-line-height: 1.35; - --nldesign-component-heading-4-color: var(--nldesign-color-text); + --nldesign-component-heading-4-color: var(--nldesign-component-heading-color); /* Heading 5 */ --nldesign-component-heading-5-font-size: 1.125rem; - --nldesign-component-heading-5-font-weight: var(--nldesign-font-weight-heading); + --nldesign-component-heading-5-font-weight: var(--nldesign-component-heading-font-weight); --nldesign-component-heading-5-line-height: 1.4; - --nldesign-component-heading-5-color: var(--nldesign-color-text); + --nldesign-component-heading-5-color: var(--nldesign-component-heading-color); /* Heading 6 */ --nldesign-component-heading-6-font-size: 1rem; - --nldesign-component-heading-6-font-weight: var(--nldesign-font-weight-heading); + --nldesign-component-heading-6-font-weight: var(--nldesign-component-heading-font-weight); --nldesign-component-heading-6-line-height: 1.5; - --nldesign-component-heading-6-color: var(--nldesign-color-text); + --nldesign-component-heading-6-color: var(--nldesign-component-heading-color); /* =================================================================== * COMPONENT TOKENS — Typography: Paragraph @@ -361,6 +371,8 @@ --nldesign-component-paragraph-font-size: 1rem; --nldesign-component-paragraph-line-height: 1.5; --nldesign-component-paragraph-color: var(--nldesign-color-text); + --nldesign-component-paragraph-font-family: var(--nldesign-font-family); + --nldesign-component-paragraph-font-weight: var(--nldesign-font-weight-default); /* =================================================================== * COMPONENT TOKENS — Link diff --git a/css/systems/nldesign/element-overrides.css b/css/systems/nldesign/element-overrides.css index cb0aa61b..3de6f447 100644 --- a/css/systems/nldesign/element-overrides.css +++ b/css/systems/nldesign/element-overrides.css @@ -154,7 +154,10 @@ header#header .header-menu__trigger { #header .header-start svg, #header .app-menu__waffle, #header .app-menu__waffle svg { - color: var(--nldesign-color-header-text) !important; + color: var( + --nldesign-component-header-color, + var(--nldesign-color-header-text) + ) !important; filter: none !important; -webkit-filter: none !important; opacity: 1 !important; @@ -529,7 +532,17 @@ button[class*='primary']:not([data-admin-theming-setting-color-picker]), button[class*='primary']:not([data-admin-theming-setting-color-picker]) *, .button-vue[class*='primary']:not([data-admin-theming-setting-color-picker]), .button-vue[class*='primary']:not([data-admin-theming-setting-color-picker]) * { - color: var(--nldesign-color-primary-text) !important; + /* The component token first, the brand token behind it. + * + * This rule claims the label AND every descendant of a primary button, at a + * specificity nothing in the scope layer can outrank — so while it read the + * BRAND token directly, no per-button label token could ever take effect. + * That is why the login button's label stayed white: its own token was + * never in the chain that painted it. */ + color: var( + --nldesign-component-button-primary-action-color, + var(--nldesign-color-primary-text) + ) !important; } /* ============================================ @@ -594,16 +607,57 @@ button[class*='primary']:not([data-admin-theming-setting-color-picker]) *, BORDER RADIUS CONSISTENCY ============================================ */ +/* Split by component, and this file is the one that decides. + * + * theme.css carries the same list under "REMOVE ALL ROUNDED CORNERS", but this + * file loads AFTER it, so whatever is written here is what a component ends up + * with. While both were one blanket rule, splitting theme.css alone changed + * nothing — every Corner row in the playground still set a token this rule + * overrode. Each token below is undeclared until an admin sets one and falls + * back to `--nldesign-border-radius`, so a sharp theme is unaffected. */ .button, button:not(.action-button), -.button-vue:not(.action-button), -input, -textarea, -select, +.button-vue:not(.action-button) { + border-radius: var( + --nldesign-component-button-border-radius, + var(--nldesign-border-radius) + ) !important; +} + +input { + border-radius: var( + --nldesign-component-textbox-border-radius, + var(--nldesign-border-radius) + ) !important; +} + +textarea { + border-radius: var( + --nldesign-component-textarea-border-radius, + var(--nldesign-border-radius) + ) !important; +} + +select { + border-radius: var( + --nldesign-component-form-select-border-radius, + var(--nldesign-border-radius) + ) !important; +} + .dropdown, -.modal-container, .popover { - border-radius: var(--nldesign-border-radius) !important; + border-radius: var( + --nldesign-component-actions-menu-border-radius, + var(--nldesign-border-radius) + ) !important; +} + +.modal-container { + border-radius: var( + --nldesign-component-dialog-border-radius, + var(--nldesign-border-radius) + ) !important; } /* @@ -659,10 +713,19 @@ body { * for absolutely positioned descendants. `overflow: visible` stays — the ribbon * is allowed to hang below the header when its height exceeds the header's. */ +/* The component token wins, then the set's own header colour, then white. The + chain is what lets the header be moved from the playground without touching + `--nldesign-color-header-background`, which a token set owns. */ #header { - background-color: var(--nldesign-color-header-background, #ffffff) !important; + background-color: var( + --nldesign-component-header-background-color, + var(--nldesign-color-header-background, #ffffff) + ) !important; background-image: none !important; - background: var(--nldesign-color-header-background, #ffffff) !important; + background: var( + --nldesign-component-header-background-color, + var(--nldesign-color-header-background, #ffffff) + ) !important; border-bottom: var(--nldesign-header-border-bottom, 0) !important; overflow: visible !important; } @@ -693,7 +756,10 @@ body { } #header nav.app-menu .app-menu-entry__label { - color: var(--nldesign-color-header-text) !important; + color: var( + --nldesign-component-header-color, + var(--nldesign-color-header-text) + ) !important; font-weight: 400 !important; } @@ -734,7 +800,10 @@ body { * and the extra bar is gone. */ #header nav.app-menu .app-menu-entry--active::before, #header nav.app-menu .app-menu-entry--active a::before { - background-color: var(--nldesign-color-header-text) !important; + background-color: var( + --nldesign-component-header-color, + var(--nldesign-color-header-text) + ) !important; } #header nav.app-menu .app-menu-entry { @@ -758,10 +827,14 @@ body { color: var(--color-primary) !important; } -/* Header branding text */ +/* Header branding text — follows the header's own colour token when one is + set, and keeps the brand primary when none is. */ #header .header-appname, #header .header-left a { - color: var(--nldesign-color-primary) !important; + color: var( + --nldesign-component-header-color, + var(--nldesign-color-primary) + ) !important; font-weight: 600 !important; } @@ -793,7 +866,10 @@ body { #header .header-right a, #header .header-right button, #header .menutoggle { - color: var(--nldesign-color-header-text) !important; + color: var( + --nldesign-component-header-color, + var(--nldesign-color-header-text) + ) !important; opacity: 1 !important; } @@ -802,7 +878,10 @@ body { so they keep their own painting. */ #header .header-right [class^='icon-'], #header .header-right svg:not([class*='avatar' i] svg):not(.user-status-icon svg) { - color: var(--nldesign-color-header-text) !important; + color: var( + --nldesign-component-header-color, + var(--nldesign-color-header-text) + ) !important; fill: currentColor !important; filter: none !important; opacity: 1 !important; @@ -810,7 +889,10 @@ body { /* Unified search */ #header .unified-search__button { - color: var(--nldesign-color-header-text) !important; + color: var( + --nldesign-component-header-color, + var(--nldesign-color-header-text) + ) !important; filter: none !important; } diff --git a/css/systems/nldesign/theme.css b/css/systems/nldesign/theme.css index fb5d0bb4..82f5f51f 100644 --- a/css/systems/nldesign/theme.css +++ b/css/systems/nldesign/theme.css @@ -67,8 +67,14 @@ body { /* Main header bar */ #header, header#header { - background: var(--nldesign-color-header-background) !important; - background-color: var(--nldesign-color-header-background) !important; + background: var( + --nldesign-component-header-background-color, + var(--nldesign-color-header-background) + ) !important; + background-color: var( + --nldesign-component-header-background-color, + var(--nldesign-color-header-background) + ) !important; overflow: visible !important; } @@ -147,7 +153,10 @@ header#header { #header *, #header .header-appname, #header .header-appname-container .header-appname { - color: var(--nldesign-color-header-text) !important; + color: var( + --nldesign-component-header-color, + var(--nldesign-color-header-text) + ) !important; } /* Header app icons — the app menu's icons are IMAGES (white SVG files), so @@ -165,7 +174,10 @@ header#header { /* User menu in header */ #header .header-menu, #header #user-menu { - color: var(--nldesign-color-header-text) !important; + color: var( + --nldesign-component-header-color, + var(--nldesign-color-header-text) + ) !important; } /* ============================================ @@ -183,7 +195,10 @@ header#header { max-width: 500px; margin: 0 auto; box-shadow: none !important; - border-radius: var(--nldesign-border-radius) !important; + border-radius: var( + --nldesign-component-login-box-border-radius, + var(--nldesign-border-radius) + ) !important; padding-top: 0 !important; /* Remove top padding so header is at top of box */ } @@ -203,7 +218,10 @@ header#header { #body-login .body-login-container > div { background: #ffffff !important; box-shadow: none !important; - border-radius: var(--nldesign-border-radius) !important; + border-radius: var( + --nldesign-component-login-box-border-radius, + var(--nldesign-border-radius) + ) !important; border: none !important; } @@ -215,7 +233,10 @@ header#header { background: #ffffff !important; background-color: #ffffff !important; box-shadow: none !important; - border-radius: var(--nldesign-border-radius) !important; + border-radius: var( + --nldesign-component-login-box-border-radius, + var(--nldesign-border-radius) + ) !important; border: none !important; backdrop-filter: none !important; -webkit-backdrop-filter: none !important; @@ -320,14 +341,30 @@ div[class*='login-box__wrapper'] > div { /* Remove all shadows from login elements */ #body-login input, +/* The login page's own shape reset. The button reads its OWN corner token + here, otherwise this rule outranks the button rule below and the Login + button chip's Corner row does nothing. */ #body-login button, #body-login .button-vue, #body-login a { box-shadow: none !important; - border-radius: var(--nldesign-border-radius) !important; + border-radius: var( + --nldesign-component-login-button-border-radius, + var(--nldesign-border-radius) + ) !important; } -/* Login box primary button */ +/* Login box primary button. + * + * The login button reads its OWN component tokens first and falls back to the + * shared primary-action ones. Without that fallback chain this rule painted the + * login button from `--nldesign-component-button-primary-action-*` — the same + * tokens the main primary button uses — so the two could not be moved apart, + * which is exactly what `--nldesign-component-login-button-*` exists to allow. + * + * The fallback is what keeps every shipped token set correct: a set that styles + * its buttons and says nothing about the login page still gets the button + * colours here, as it did before. */ #body-login .button-vue--vue-primary, #body-login .button-vue--primary, #body-login input[type='submit'], @@ -586,7 +623,10 @@ a:visited { #header a, #header a:hover, #header a:visited { - color: var(--nldesign-color-header-text) !important; + color: var( + --nldesign-component-header-color, + var(--nldesign-color-header-text) + ) !important; text-decoration: none !important; } @@ -685,10 +725,19 @@ label, CHECKBOXES & RADIO BUTTONS ============================================ */ +/* The checkbox reads its own token first so it can be moved without moving the + brand primary, and falls back to the primary so every existing token set is + unaffected. */ input[type='checkbox']:checked, input[type='radio']:checked { - background-color: var(--nldesign-color-primary) !important; - border-color: var(--nldesign-color-primary) !important; + background-color: var( + --nldesign-component-checkbox-checked-background-color, + var(--nldesign-color-primary) + ) !important; + border-color: var( + --nldesign-component-checkbox-checked-background-color, + var(--nldesign-color-primary) + ) !important; } /* ============================================ @@ -697,7 +746,10 @@ input[type='radio']:checked { .icon-loading, .icon-loading-small { - border-color: var(--nldesign-color-primary) !important; + border-color: var( + --nldesign-component-progress-background-color, + var(--nldesign-color-primary) + ) !important; } /* ============================================ @@ -747,11 +799,25 @@ h6 { color: var(--nldesign-component-heading-6-color) !important; } +/* Typeface for headings, as one rule rather than six: the six per-level tokens + cover size, weight, line-height and colour, and a brand that gives its + headings a display face gives it to all of them. */ +h1, +h2, +h3, +h4, +h5, +h6 { + font-family: var(--nldesign-component-heading-font-family) !important; +} + /* Paragraphs */ p { font-size: var(--nldesign-component-paragraph-font-size) !important; line-height: var(--nldesign-component-paragraph-line-height) !important; color: var(--nldesign-component-paragraph-color) !important; + font-family: var(--nldesign-component-paragraph-font-family) !important; + font-weight: var(--nldesign-component-paragraph-font-weight) !important; } /* Tables */ @@ -785,7 +851,10 @@ hr { ============================================ */ .toastify.dialogs { - border-radius: var(--nldesign-border-radius) !important; + border-radius: var( + --nldesign-component-toast-border-radius, + var(--nldesign-border-radius) + ) !important; } /* ============================================ @@ -931,22 +1000,68 @@ button:not(.action-button), * instance therefore rendered every avatar — the user menu in the header, * the contacts menu, share recipients — as a 3px-cornered SQUARE, which is not * a brand choice any token set made. File previews and app icons were squared - * off the same way. Components that need the brand radius are listed by name. */ + * off the same way. Components that need the brand radius are listed by name. + * + * SPLIT BY COMPONENT, and that is what makes the corner rows work. + * + * This was one rule forcing `--nldesign-border-radius` onto every selector + * below, which outranked each component's own radius rule — so the Corner row + * on seven chips set a token nothing read. Every component token here is + * UNDECLARED until someone sets one and falls back to `--nldesign-border-radius`, + * so a sharp theme still squares off exactly as before. */ .panel, .widget, -.button-vue:not(.action-button), -button:not(.action-button), -.modal-container, -.app-navigation-entry, -input, -textarea, -select, -.dropdown, -.popover, .tooltip { border-radius: var(--nldesign-border-radius) !important; } +.button-vue:not(.action-button), +button:not(.action-button) { + border-radius: var( + --nldesign-component-button-border-radius, + var(--nldesign-border-radius) + ) !important; +} + +.modal-container { + border-radius: var( + --nldesign-component-dialog-border-radius, + var(--nldesign-border-radius) + ) !important; +} + +.app-navigation-entry { + border-radius: var( + --nldesign-component-navigation-border-radius, + var(--nldesign-border-radius) + ) !important; +} + +input { + border-radius: var( + --nldesign-component-textbox-border-radius, + var(--nldesign-border-radius) + ) !important; +} + +textarea { + border-radius: var( + --nldesign-component-textarea-border-radius, + var(--nldesign-border-radius) + ) !important; +} + +/* `select` is not repeated here: its own rule in FORM ELEMENTS above already + reads `--nldesign-component-form-select-border-radius`. */ + +.dropdown, +.popover { + border-radius: var( + --nldesign-component-actions-menu-border-radius, + var(--nldesign-border-radius) + ) !important; +} + /* ============================================ FONT APPLICATION ============================================ */ From 7bcb7e6f72783a2b95089204d93377a80311a478 Mon Sep 17 00:00:00 2001 From: Remko Date: Wed, 23 Sep 2026 13:23:41 +0200 Subject: [PATCH 10/72] feat(tokens): serve the component layer and gate it behind a setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- appinfo/routes.php | 1 + lib/Controller/SettingsController.php | 34 +++++++ lib/Service/ConfigBundleService.php | 14 +++ lib/Service/CssInjectionService.php | 24 ++++- lib/Service/TokenRegistry.php | 131 +++++++++++++++++++++++-- lib/Service/TokenRegistryInterface.php | 6 +- lib/Settings/Admin.php | 14 +++ 7 files changed, 214 insertions(+), 10 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index d031390f..874a1478 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -32,6 +32,7 @@ ['name' => 'settings#getTokenSet', 'url' => '/settings/tokenset', 'verb' => 'GET'], ['name' => 'settings#setSloganSetting', 'url' => '/settings/slogan', 'verb' => 'POST'], ['name' => 'settings#setMenuLabelsSetting', 'url' => '/settings/menulabels', 'verb' => 'POST'], + ['name' => 'settings#setPrimaryDrivesComponentsSetting', 'url' => '/settings/primary-drives-components', 'verb' => 'POST'], ['name' => 'settings#getThemingValues', 'url' => '/settings/theming', 'verb' => 'GET'], ['name' => 'settings#updateThemingValues', 'url' => '/settings/theming', 'verb' => 'POST'], // Resetting core theming to stock deliberately has NO route of its own: diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index f7e3d839..4948c6f9 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -323,6 +323,40 @@ public function setSloganSetting(bool $hideSlogan): JSONResponse { return new JSONResponse(['status' => 'ok', 'hideSlogan' => $hideSlogan]); }//end setSloganSetting() + /** + * Set whether the brand primary overrules the component tokens it used to drive. + * + * Turning this on emits `css/primary-lock.css`, which forces every component + * token flagged `primary` in `scripts/mapping/component-tokens.json` back to + * the brand value. Stored per-component values are left untouched, so turning + * it off restores them. + * + * @param bool $primaryDrivesComponents Whether the primary overrules component tokens. + * + * @return JSONResponse The response with the status. + * + * @spec openspec/specs/component-tokens/spec.md + * @spec openspec/specs/theming-audit/spec.md#requirement-complete-call-site-coverage + */ + #[AuthorizedAdminSetting(Admin::class)] + public function setPrimaryDrivesComponentsSetting(bool $primaryDrivesComponents): JSONResponse { + $previous = ($this->config->getAppValue(Application::APP_ID, 'primary_drives_components', '0') === '1'); + $this->saveBooleanSetting(key: 'primary_drives_components', value: $primaryDrivesComponents); + + $this->auditService->log( + action: 'toggle_changed', + context: [ + 'key' => 'primary_drives_components', + 'old' => $previous, + 'new' => $primaryDrivesComponents, + ] + ); + + return new JSONResponse( + ['status' => 'ok', 'primaryDrivesComponents' => $primaryDrivesComponents] + ); + }//end setPrimaryDrivesComponentsSetting() + /** * Set the show menu labels setting. * diff --git a/lib/Service/ConfigBundleService.php b/lib/Service/ConfigBundleService.php index 977e6c95..ff54ac9c 100644 --- a/lib/Service/ConfigBundleService.php +++ b/lib/Service/ConfigBundleService.php @@ -251,6 +251,7 @@ public function export(): array { 'tokenSet' => $this->config->getAppValue(Application::APP_ID, 'token_set', 'nextcloud'), 'hideSlogan' => ($this->config->getAppValue(Application::APP_ID, 'hide_slogan', '0') === '1'), 'showMenuLabels' => ($this->config->getAppValue(Application::APP_ID, 'show_menu_labels', '0') === '1'), + 'primaryDrivesComponents' => ($this->config->getAppValue(Application::APP_ID, 'primary_drives_components', '0') === '1'), 'disabledApps' => $this->appThemingService->getDisabledApps(), 'upstreamFreshnessEnabled' => $this->freshnessService->isEnabled(), ], @@ -440,6 +441,11 @@ private function validateConfigSection(array $bundle, array &$errors): array { $errors[] = ['section' => 'config', 'message' => '"config.showMenuLabels" must be a boolean.']; } + $primaryDrivesComponents = ($config['primaryDrivesComponents'] ?? false); + if (is_bool($primaryDrivesComponents) === false) { + $errors[] = ['section' => 'config', 'message' => '"config.primaryDrivesComponents" must be a boolean.']; + } + $disabledApps = $this->validateDisabledApps(config: $config, errors: $errors); $upstreamFreshnessEnabled = ($config['upstreamFreshnessEnabled'] ?? false); @@ -451,6 +457,7 @@ private function validateConfigSection(array $bundle, array &$errors): array { 'tokenSet' => $tokenSet, 'hideSlogan' => ($hideSlogan === true), 'showMenuLabels' => ($showMenuLabels === true), + 'primaryDrivesComponents' => ($primaryDrivesComponents === true), 'disabledApps' => $disabledApps, 'upstreamFreshnessEnabled' => ($upstreamFreshnessEnabled === true), ]; @@ -840,6 +847,7 @@ private function buildSectionSummary(array $resolved): array { 'tokenSet' => $resolved['config']['tokenSet'], 'hideSlogan' => $resolved['config']['hideSlogan'], 'showMenuLabels' => $resolved['config']['showMenuLabels'], + 'primaryDrivesComponents' => $resolved['config']['primaryDrivesComponents'], 'disabledAppsCount' => count($resolved['config']['disabledApps']), 'upstreamFreshnessEnabled' => $resolved['config']['upstreamFreshnessEnabled'], ], @@ -884,9 +892,15 @@ private function apply(array $resolved): void { $showMenuLabelsValue = '1'; } + $primaryDrivesComponentsValue = '0'; + if ($config['primaryDrivesComponents'] === true) { + $primaryDrivesComponentsValue = '1'; + } + $this->config->setAppValue(Application::APP_ID, 'token_set', $config['tokenSet']); $this->config->setAppValue(Application::APP_ID, 'hide_slogan', $hideSloganValue); $this->config->setAppValue(Application::APP_ID, 'show_menu_labels', $showMenuLabelsValue); + $this->config->setAppValue(Application::APP_ID, 'primary_drives_components', $primaryDrivesComponentsValue); $this->appThemingService->setDisabledApps(appIds: $config['disabledApps']); $this->freshnessService->setEnabled(enabled: $config['upstreamFreshnessEnabled']); diff --git a/lib/Service/CssInjectionService.php b/lib/Service/CssInjectionService.php index 5490cf7a..3cbc22b6 100644 --- a/lib/Service/CssInjectionService.php +++ b/lib/Service/CssInjectionService.php @@ -232,8 +232,8 @@ private function runLayer(string $layer, callable $work): void { * appconfig (see {@see isContextThemed()}). Otherwise this is the * verbatim former `Application::injectThemeCSS()` body: design-system * stylesheets in declared order, token set CSS, icon/error contrast - * fixes, custom overrides, custom fonts, then conditional - * hide-slogan/show-menu-labels stylesheets. + * fixes, the component scopes, custom overrides, custom fonts, then + * conditional hide-slogan/show-menu-labels/primary-lock stylesheets. * * @param string $context One of `user`/`login`/`guest`/`public`/`error`, * or any other value (always themed — fail open). @@ -280,6 +280,11 @@ public function inject(string $context): void { work: fn () => $this->injectDesignSystemStyles(designSystemId: $designSystemId, tokenSet: $tokenSet) ); + // 3.9. Component scopes. Must sit 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. + $this->runLayer(layer: 'component-scopes', work: fn () => $this->emitStyle(file: 'component-scopes')); + // 4/4.1. Custom overrides, then freeform custom CSS. $this->runLayer(layer: 'override-styles', work: fn () => $this->injectOverrideStyles()); @@ -604,11 +609,20 @@ private function injectCustomFontLink(string $designSystemId): void { }//end injectCustomFontLink() /** - * Emit the appconfig-gated hide-slogan and show-menu-labels stylesheets. + * Emit the appconfig-gated hide-slogan, show-menu-labels and primary-lock + * stylesheets. + * + * `primary-lock` is emitted LAST on purpose. It and `custom-overrides.css` + * both write `--nldesign-component-*` at `:root` with `!important`, so the + * later of the two wins — and while the setting is on, the brand primary is + * meant to beat a per-component value the admin stored earlier. Nothing is + * deleted: turning the setting off drops this layer and the stored values + * take effect again. * * @return void * * @spec openspec/specs/css-architecture/spec.md + * @spec openspec/specs/component-tokens/spec.md */ private function injectConditionalStyles(): void { if ($this->config->getAppValue(Application::APP_ID, 'hide_slogan', '0') === '1') { @@ -618,6 +632,10 @@ private function injectConditionalStyles(): void { if ($this->config->getAppValue(Application::APP_ID, 'show_menu_labels', '0') === '1') { $this->emitStyle(file: 'show-menu-labels'); } + + if ($this->config->getAppValue(Application::APP_ID, 'primary_drives_components', '0') === '1') { + $this->emitStyle(file: 'primary-lock'); + } }//end injectConditionalStyles() /** diff --git a/lib/Service/TokenRegistry.php b/lib/Service/TokenRegistry.php index e5a5aebd..bc010f5f 100644 --- a/lib/Service/TokenRegistry.php +++ b/lib/Service/TokenRegistry.php @@ -23,7 +23,7 @@ namespace OCA\Thematiq\Service; /** - * Canonical registry of editable Nextcloud CSS custom properties. + * Canonical registry of editable CSS custom properties. * * This class is the single source of truth for: * - Which tokens the editor exposes for editing @@ -31,35 +31,154 @@ * - Which type of input to render (color picker or text field) * - The human-readable label for each token * + * TWO LAYERS, AND THE DIFFERENCE MATTERS + * -------------------------------------- + * BRAND tokens are Nextcloud's own globals — `--color-primary`, + * `--color-background-hover`, `--border-radius-element`. They are hand-listed in + * the four methods below, and moving one is MEANT to move every component that + * has not been given a value of its own. + * + * COMPONENT tokens are `--nldesign-component-*`, and they are not listed here at + * all: they are read from `scripts/mapping/component-tokens.json`, the same file + * `scripts/generate-component-scopes.mjs` turns into `css/component-scopes.css`. + * One table, two runtimes, so a token cannot be editable without a stylesheet + * that applies it — or applied by a stylesheet with no way to edit it. + * + * WHY THE COMPONENT LAYER EXISTS. Nextcloud has around sixty globals and every + * component draws from them, so `--color-primary-element` paints the primary + * button, the selected navigation entry, the sidebar's active tab, a focused + * text input, the checked checkbox, the progress bar, the dialog's confirm + * button and the counter bubble. Before this layer the editor could only write + * that global, so the playground's `Primary button` chip moved all eight. + * * Tokens marked "intentionally not overridden" in overrides.css MUST NOT appear here. * The excluded list covers dark-mode vars, auto-calculated values, and layout constants. * * Tabs: login | content | status | typography * Types: color | text + * Groups: `brand`, or the id of the component the token belongs to * * @spec openspec/changes/retrofit-2026-05-24-annotate-nldesign/tasks.md#task-45 * @spec openspec/changes/retrofit-2026-05-24-annotate-nldesign/tasks.md#task-46 * @spec openspec/changes/retrofit-2026-05-24-annotate-nldesign/tasks.md#task-47 */ class TokenRegistry implements TokenRegistryInterface { + /** + * The component-token table, relative to the app root. + * + * Read rather than injected because this class is static by design and the + * table is a shipped file, not configuration. `__DIR__` is `lib/Service`. + */ + private const COMPONENT_TOKENS_PATH = __DIR__ . '/../../scripts/mapping/component-tokens.json'; + + /** + * Decoded component-token table, or null before the first read. + * + * The registry is asked for on every admin page render and the table is + * parsed from disk, so the result is kept for the rest of the request. + * + * @var array|null + */ + private static ?array $componentTokens = null; + /** * Returns the full registry of editable tokens. * * Keys are CSS custom property names (e.g. '--color-primary'). - * Values are arrays with 'tab', 'type', and 'label' keys. + * Values carry 'tab', 'type', 'label', 'group' and 'primary'. * - * @return array The token registry. + * @return array The token registry. * * @spec openspec/changes/retrofit-2026-05-24-annotate-nldesign/tasks.md#task-45 */ public static function getTokens(): array { - return array_merge( + return array_merge(self::getBrandTokens(), self::getComponentTokens()); + }//end getTokens() + + /** + * Returns the brand layer: Nextcloud's own global custom properties. + * + * These are the values every component falls back to. They carry no + * component of their own, so they are grouped under `brand` and are never + * locked by the `primary_drives_components` setting — that setting exists to + * make these win, not to freeze them. + * + * @return array The brand tokens. + * + * @spec openspec/specs/component-tokens/spec.md + */ + public static function getBrandTokens(): array { + $tokens = array_merge( self::getLoginTokens(), self::getContentTokens(), self::getStatusTokens(), self::getTypographyTokens() ); - }//end getTokens() + + return array_map( + static fn (array $meta): array => array_merge($meta, ['group' => 'brand', 'primary' => false]), + $tokens + ); + }//end getBrandTokens() + + /** + * Returns the component layer, read from the shared mapping table. + * + * A missing or malformed table degrades to an empty component layer rather + * than an error: the brand tokens still edit, and the instance still renders, + * because `css/component-scopes.css` falls back to the captured global for + * every component token nobody set. + * + * @return array The component tokens. + * + * @spec openspec/specs/component-tokens/spec.md + */ + public static function getComponentTokens(): array { + if (self::$componentTokens !== null) { + return self::$componentTokens; + } + + self::$componentTokens = []; + + $raw = false; + if (is_file(self::COMPONENT_TOKENS_PATH) === true) { + $raw = file_get_contents(self::COMPONENT_TOKENS_PATH); + } + + if ($raw === false) { + return self::$componentTokens; + } + + $decoded = json_decode($raw, true); + if (is_array($decoded) === false || is_array($decoded['components'] ?? null) === false) { + return self::$componentTokens; + } + + foreach ($decoded['components'] as $componentId => $component) { + if (is_array($component['tokens'] ?? null) === false) { + continue; + } + + foreach ($component['tokens'] as $name => $meta) { + self::$componentTokens[$name] = [ + 'tab' => (string)($component['tab'] ?? 'content'), + 'type' => (string)($meta['type'] ?? 'text'), + 'label' => (string)($meta['label'] ?? $name), + 'group' => (string)$componentId, + 'primary' => (($meta['primary'] ?? false) === true), + // The Nextcloud variable this token replaces inside the + // component. A component token is deliberately undeclared + // until someone sets one, so `getComputedStyle` reports it + // as the empty string — the editor reads this global + // instead, and shows the colour the component is ACTUALLY + // wearing rather than a blank swatch. + 'global' => (string)($meta['global'] ?? ''), + ]; + } + } + + return self::$componentTokens; + }//end getComponentTokens() /** * Returns login and branding tab tokens. @@ -208,7 +327,7 @@ public static function isEditable(string $tokenName): bool { /** * Returns tokens grouped by tab. * - * @return array> Tokens grouped by tab id. + * @return array> Tokens grouped by tab id. * * @spec openspec/changes/retrofit-2026-05-24-annotate-nldesign/tasks.md#task-46 */ diff --git a/lib/Service/TokenRegistryInterface.php b/lib/Service/TokenRegistryInterface.php index a4994b38..068e79db 100644 --- a/lib/Service/TokenRegistryInterface.php +++ b/lib/Service/TokenRegistryInterface.php @@ -27,7 +27,11 @@ interface TokenRegistryInterface { /** * Returns the full registry of editable tokens. * - * @return array The token registry. + * `group` is `brand` for Nextcloud's own globals and the component id for a + * `--nldesign-component-*` token; `primary` marks a component token the brand + * primary used to drive, which the `primary_drives_components` setting locks. + * + * @return array The token registry. * * @spec openspec/changes/retrofit-2026-05-24-annotate-nldesign/tasks.md#task-45 */ diff --git a/lib/Settings/Admin.php b/lib/Settings/Admin.php index 4b69d872..2d5421a1 100644 --- a/lib/Settings/Admin.php +++ b/lib/Settings/Admin.php @@ -205,6 +205,19 @@ public function getForm(): TemplateResponse { '0' ) === '1'; + // Whether the brand primary overrules every component token it used to + // drive. OFF by default, and that costs nothing visually: with no + // per-component value stored, the component tokens already resolve to the + // brand primary, so an instance that has never opened the playground + // renders identically either way. Turning it ON is the deliberate choice + // to give that back up — the primary wins again and the per-component + // colour controls lock. + $primaryDrivesComponents = $this->config->getAppValue( + Application::APP_ID, + 'primary_drives_components', + '0' + ) === '1'; + $darkVariantsEnabled = $this->config->getAppValue( Application::APP_ID, 'dark_variants', @@ -261,6 +274,7 @@ public function getForm(): TemplateResponse { 'currentDesignSystem' => $currentDesignSystem, 'hideSlogan' => $hideSlogan, 'showMenuLabels' => $showMenuLabels, + 'primaryDrivesComponents' => $primaryDrivesComponents, 'darkVariantsEnabled' => $darkVariantsEnabled, 'marianneEnabled' => $marianneEnabled, 'emailThemingState' => $emailThemingState, From 7292fbb88a69fbe7ca6cc6502c8b9b0fbd53ca12 Mon Sep 17 00:00:00 2001 From: Remko Date: Wed, 23 Sep 2026 13:24:21 +0200 Subject: [PATCH 11/72] feat(settings): group the editor by component and lock the driven rows 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. --- css/admin.css | 11 ++ js/admin.js | 357 ++++++++++++++++++++++++++++------- l10n/be.js | 5 +- l10n/be.json | 5 +- l10n/bg.js | 5 +- l10n/bg.json | 5 +- l10n/bs.js | 5 +- l10n/bs.json | 5 +- l10n/ca.js | 5 +- l10n/ca.json | 5 +- l10n/cs.js | 5 +- l10n/cs.json | 5 +- l10n/da.js | 5 +- l10n/da.json | 5 +- l10n/de.js | 5 +- l10n/de.json | 5 +- l10n/el.js | 5 +- l10n/el.json | 5 +- l10n/en.js | 5 +- l10n/en.json | 5 +- l10n/es.js | 5 +- l10n/es.json | 5 +- l10n/et.js | 5 +- l10n/et.json | 5 +- l10n/fi.js | 5 +- l10n/fi.json | 5 +- l10n/fr.js | 5 +- l10n/fr.json | 5 +- l10n/ga.js | 5 +- l10n/ga.json | 5 +- l10n/hr.js | 5 +- l10n/hr.json | 5 +- l10n/hu.js | 5 +- l10n/hu.json | 5 +- l10n/is.js | 5 +- l10n/is.json | 5 +- l10n/it.js | 5 +- l10n/it.json | 5 +- l10n/lb.js | 5 +- l10n/lb.json | 5 +- l10n/lt.js | 5 +- l10n/lt.json | 5 +- l10n/lv.js | 5 +- l10n/lv.json | 5 +- l10n/mk.js | 5 +- l10n/mk.json | 5 +- l10n/mt.js | 5 +- l10n/mt.json | 5 +- l10n/nb.js | 5 +- l10n/nb.json | 5 +- l10n/nl.js | 5 +- l10n/nl.json | 5 +- l10n/pl.js | 5 +- l10n/pl.json | 5 +- l10n/pt.js | 5 +- l10n/pt.json | 5 +- l10n/rm.js | 5 +- l10n/rm.json | 5 +- l10n/ro.js | 5 +- l10n/ro.json | 5 +- l10n/ru.js | 5 +- l10n/ru.json | 5 +- l10n/sk.js | 5 +- l10n/sk.json | 5 +- l10n/sl.js | 5 +- l10n/sl.json | 5 +- l10n/sq.js | 5 +- l10n/sq.json | 5 +- l10n/sr.js | 5 +- l10n/sr.json | 5 +- l10n/sv.js | 5 +- l10n/sv.json | 5 +- l10n/tr.js | 5 +- l10n/tr.json | 5 +- l10n/uk.js | 5 +- l10n/uk.json | 5 +- templates/settings/admin.php | 21 +++ 77 files changed, 616 insertions(+), 143 deletions(-) diff --git a/css/admin.css b/css/admin.css index 393930de..aaf7bbb5 100644 --- a/css/admin.css +++ b/css/admin.css @@ -555,6 +555,17 @@ padding: 7px 0; border-bottom: 1px solid var(--color-border); } +/* A row the "primary drives every component" setting has taken ownership of. + * + * 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` is on the row rather than + * the input because a disabled input does not fire pointer events, so the + * cursor would never change over the control itself. */ +.nldesign-token-row--locked { + opacity: 0.55; + cursor: not-allowed; +} .nldesign-token-label-wrap { flex: 1 1 auto; min-width: 0; diff --git a/js/admin.js b/js/admin.js index 8d0b21cd..9a289c8f 100644 --- a/js/admin.js +++ b/js/admin.js @@ -95,8 +95,39 @@ var settingsEl = document.getElementById('nldesign-settings') var tokenSetSelect = document.getElementById('nldesign-token-set-select') var hideSloganCheckbox = document.getElementById('nldesign-hide-slogan') + var primaryDrivesComponentsCheckbox = document.getElementById( + 'nldesign-primary-drives-components', + ) var previewRoot = document.getElementById('nldesign-preview') + // Whether the brand primary currently overrules the component tokens it + // used to drive. Read off the server-rendered checkbox rather than through + // initial state: the template already carries the value, and the flag has + // to stay in step with the control the admin is looking at. + var primaryDrivesComponents = + primaryDrivesComponentsCheckbox !== null + && primaryDrivesComponentsCheckbox.checked === true + + /** + * Whether a token's control is locked by the "primary drives every + * component" setting. + * + * Only component tokens carry `primary`. The brand tokens never lock — + * that setting exists to make THEM win, so freezing them would leave the + * admin with no way to change anything at all. + * + * @param {string} name The CSS custom property name. + * + * @return {boolean} True when the row must render disabled. + */ + function isTokenLocked(name) { + if (primaryDrivesComponents === false) { + return false + } + var meta = tokenRegistry[name] + return meta !== undefined && meta.primary === true + } + // Read a live CSS custom property with a fallback. Reads from first // (Nextcloud and the nldesign themes set their token vars there), then :root. function readVar(name, fallback) { @@ -619,81 +650,103 @@ // the live NC token values. Primary comes from the selected set's metadata // (it isn't applied until confirmed); the remaining colours mirror the live // theme, so the token-editor pickers reflect into the preview too. + /** + * Repaint the rich preview from the live CSS stack. + * + * READ EVERYTHING, THEN WRITE EVERYTHING — and that split is the whole + * performance of this panel. + * + * This used to interleave the two: read a variable, set a `--prev-*`, + * read the next. Writing to `previewRoot.style` invalidates style for + * the subtree, so the read after it could not be served from the last + * computation and forced a fresh synchronous one. Eighteen properties + * meant eighteen full style recalculations, against a cascade that + * carries theme.css, element-overrides.css, component-scopes.css and + * the vendored guest sheet. The colour picker fires `input` on every + * pixel of a drag, so that ran dozens of times a second — which is what + * made the picker stutter. + * + * Reading first means one recalculation for the batch, and the writes + * afterwards cost nothing until the next frame paints. + * + * @param {string} tokenSet The token set id the preview is drawn for. + * + * @return {void} + */ function updatePreview(tokenSet) { if (!previewRoot) { return } + var colors = getPreviewColors(tokenSet) - var s = previewRoot.style - s.setProperty('--prev-primary', colors.primary) - s.setProperty( - '--prev-primary-text', - colors.primaryText || readVar('--color-primary-text', '#ffffff'), - ) - s.setProperty( - '--prev-surface', - readVar('--color-main-background', '#ffffff'), - ) - s.setProperty('--prev-bg', readVar('--color-background-dark', '#f2f4f7')) - s.setProperty('--prev-text', readVar('--color-main-text', '#1b2733')) - s.setProperty( - '--prev-muted', - readVar('--color-text-maxcontrast', '#6b7785'), - ) - s.setProperty('--prev-border', readVar('--color-border', '#e3e9f0')) - s.setProperty('--prev-warning', readVar('--color-warning', '#c79a00')) - s.setProperty('--prev-error', readVar('--color-error', '#c0392b')) - s.setProperty( - '--prev-info', - readVar( + + // ---- read phase ------------------------------------------------- + // Both declarations are resolved once here instead of per lookup; + // `readVar` builds a new pair on every call. + var bodyStyle = getComputedStyle(document.body) + var rootStyle = getComputedStyle(document.documentElement) + + function read(name, fallback) { + var value = (bodyStyle.getPropertyValue(name) || '').trim() + if (value === '') { + value = (rootStyle.getPropertyValue(name) || '').trim() + } + return value || fallback + } + + var primaryText = + colors.primaryText || read('--color-primary-text', '#ffffff') + + var values = { + '--prev-primary': colors.primary, + '--prev-primary-text': primaryText, + '--prev-surface': read('--color-main-background', '#ffffff'), + '--prev-bg': read('--color-background-dark', '#f2f4f7'), + '--prev-text': read('--color-main-text', '#1b2733'), + '--prev-muted': read('--color-text-maxcontrast', '#6b7785'), + '--prev-border': read('--color-border', '#e3e9f0'), + '--prev-warning': read('--color-warning', '#c79a00'), + '--prev-error': read('--color-error', '#c0392b'), + '--prev-info': read( '--color-info', - readVar('--color-primary-element', colors.primary), + read('--color-primary-element', colors.primary), ), - ) - s.setProperty('--prev-radius', readVar('--border-radius-element', '8px')) - // The rounded container Nextcloud clips the navigation and the app - // content into, and the pill radius of a navigation entry. - s.setProperty( - '--prev-radius-container', - readVar( + '--prev-radius': read('--border-radius-element', '8px'), + // The rounded container Nextcloud clips the navigation and the + // app content into, and the pill radius of a navigation entry. + '--prev-radius-container': read( '--body-container-radius', - readVar('--border-radius-large', '12px'), + read('--border-radius-large', '12px'), ), - ) - s.setProperty( - '--prev-radius-pill', - readVar('--border-radius-pill', '999px'), - ) - // The page background BEHIND the content container — visible in the - // gap around it, which is where a themed instance shows its plain - // colour or background image. - s.setProperty( - '--prev-plain', - readVar( + '--prev-radius-pill': read('--border-radius-pill', '999px'), + // The page background BEHIND the content container — visible in + // the gap around it, which is where a themed instance shows its + // plain colour or background image. + '--prev-plain': read( '--color-background-plain', - readVar('--color-background-dark', '#f2f4f7'), + read('--color-background-dark', '#f2f4f7'), ), - ) - // The header is its own role: a set may paint it differently from the - // primary (Cunningham and Amsterdam both paint it white, over a blue - // primary). Fall back to the primary only when the page carries no - // header token. - s.setProperty( - '--prev-header-bg', - readVar('--nldesign-color-header-background', colors.primary), - ) - s.setProperty( - '--prev-header-text', - readVar( + // The header is its own role: a set may paint it differently + // from the primary (Cunningham and Amsterdam both paint it white + // over a blue primary). Fall back to the primary only when the + // page carries no header token. + '--prev-header-bg': read( + '--nldesign-color-header-background', + colors.primary, + ), + '--prev-header-text': read( '--nldesign-color-header-text', - colors.primaryText || readVar('--color-primary-text', '#ffffff'), + primaryText, ), - ) - s.setProperty( - '--prev-header-border', - readVar('--nldesign-header-border-bottom', '0'), - ) - s.setProperty('--prev-login-bg', colors.primary) + '--prev-header-border': read('--nldesign-header-border-bottom', '0'), + '--prev-login-bg': colors.primary, + } + + // ---- write phase ------------------------------------------------ + var s = previewRoot.style + Object.keys(values).forEach(function (name) { + s.setProperty(name, values[name]) + }) } // App / Login preview switch. @@ -2012,6 +2065,16 @@ }) } + // Handle the "primary drives every component" checkbox + if (primaryDrivesComponentsCheckbox) { + primaryDrivesComponentsCheckbox.addEventListener( + 'change', + function () { + savePrimaryDrivesComponentsSetting(this.checked) + }, + ) + } + // Handle show menu labels checkbox var showMenuLabelsCheckbox = document.getElementById( 'nldesign-show-menu-labels', @@ -2157,6 +2220,71 @@ }) } + // Save the "primary drives every component" setting to the server, then + // bring this page into the state it just asked for: add or drop + // primary-lock.css so the specimens and the playground repaint, and + // re-render the editor so the rows the primary now owns lock or unlock. + function savePrimaryDrivesComponentsSetting(enabled) { + var url = OC.generateUrl( + '/apps/thematiq/settings/primary-drives-components', + ) + + fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + requesttoken: OC.requestToken, + }, + body: JSON.stringify({ primaryDrivesComponents: enabled }), + }) + .then(function (response) { + return response.json() + }) + .then(function (data) { + if (data.status === 'ok') { + primaryDrivesComponents = enabled === true + setConditionalLayer('primary-lock', primaryDrivesComponents) + refreshTokenEditorLocks() + notify(t('thematiq', 'Applied.')) + } else { + notify(t('thematiq', 'Failed to save setting.')) + } + }) + .catch(function (error) { + console.error( + 'Error saving primary-drives-components setting:', + error, + ) + notify(t('thematiq', 'Failed to save setting.')) + }) + } + + /** + * Lock or unlock the editor rows the primary owns, in place. + * + * Deliberately not a re-render: the editor holds unsaved edits in + * `tokenEditorState`, and rebuilding the panels would drop them and reset + * the open tab. Only the two attributes that express the lock are touched. + * + * @return {void} + */ + function refreshTokenEditorLocks() { + var container = document.getElementById('nldesign-token-editor') + if (container === null) { + return + } + + container + .querySelectorAll('.nldesign-token-row') + .forEach(function (row) { + var locked = isTokenLocked(row.dataset.tokenRow) + row.classList.toggle('nldesign-token-row--locked', locked) + row.querySelectorAll('input').forEach(function (input) { + input.disabled = locked + }) + }) + } + // Save show menu labels setting to server. function saveMenuLabelsSetting(showMenuLabels) { var url = OC.generateUrl('/apps/thematiq/settings/menulabels') @@ -2200,6 +2328,45 @@ // Tab labels from server: tab id → display label var tokenTabLabels = {} + /** + * The value a token row should show. + * + * A component token is deliberately UNDECLARED until somebody sets one: + * that is what makes `var(--nldesign-component-x, var(--global))` in + * css/component-scopes.css fall through to the brand value, and what + * makes an untouched instance render exactly as it did before the + * component layer existed. The cost is that `getPropertyValue` reports + * it as the empty string, so every one of those rows opened blank — + * a white swatch beside a component that is plainly not white. + * + * So when the token itself is unset, the row shows the Nextcloud + * variable it falls back to: the colour the component is ACTUALLY + * wearing. The registry carries that name as `global`. + * + * This is display only. `saveOverrides()` writes a token only when its + * value differs from `resolved`, and `resolved` is what this returned — + * so showing the inherited colour never writes it, and the fallback + * stays live until the admin actually picks something. + * + * @param {CSSStyleDeclaration} rootStyle Computed style of the document element. + * @param {string} name The CSS custom property name. + * + * @return {string} The resolved value, or its fallback, or ''. + */ + function resolveTokenValue(rootStyle, name) { + var own = rootStyle.getPropertyValue(name).trim() + if (own !== '') { + return own + } + + var meta = tokenRegistry[name] + if (meta === undefined || !meta.global) { + return '' + } + + return rootStyle.getPropertyValue(meta.global).trim() + } + /** * Initialise and mount the token editor panel into #nldesign-token-editor. */ @@ -2223,7 +2390,7 @@ // Read resolved values from the live CSS stack. var rootStyle = getComputedStyle(document.documentElement) Object.keys(tokenRegistry).forEach(function (name) { - var resolved = rootStyle.getPropertyValue(name).trim() + var resolved = resolveTokenValue(rootStyle, name) var overridden = overrides[name] !== undefined ? overrides[name] : null tokenEditorState[name] = { @@ -2397,6 +2564,22 @@ }), ) + // Locked rows render disabled rather than hidden. The value is still + // stored and still shown, because turning the setting back off restores + // it — hiding the control would make a kept value look like a lost one. + // `disabled` also carries to the playground, which clones these rows. + var locked = isTokenLocked(name) + var lockedAttr = locked + ? ' disabled title="' + + escapeHtml( + t( + 'thematiq', + 'The primary colour drives this component. Switch off "Let the primary colour drive every component" to set it separately.', + ), + ) + + '"' + : '' + var inputHtml = '' if (meta.type === 'color') { var pickerVal = normaliseColorForPicker(displayVal) @@ -2408,14 +2591,18 @@ + escapeHtml(name) + '" value="' + escapeHtml(pickerVal) - + '">' + + '"' + + lockedAttr + + '>' + '' + + '"' + + lockedAttr + + '>' + '' } else { inputHtml = @@ -2425,11 +2612,15 @@ + escapeHtml(name) + '" value="' + escapeHtml(displayVal) - + '">' + + '"' + + lockedAttr + + '>' } return ( - '
' + '
' @@ -2555,14 +2746,42 @@ }) } + // Pending preview repaint, so a drag cannot queue more than one a frame. + var previewFrame = null + + /** + * Ask for a preview repaint at the next frame, at most one per frame. + * + * A colour input fires `input` continuously while the pointer is down — + * well above 60 a second on a fast mouse — and each repaint reads the + * computed cascade. Repainting per event did work the frame could never + * show, because several events land between two paints and only the + * last one is visible. Coalescing keeps the preview exactly as current + * while doing a fraction of the work. + * + * @return {void} + */ + function schedulePreviewRepaint() { + if (previewFrame !== null) { + return + } + previewFrame = requestAnimationFrame(function () { + previewFrame = null + updatePreview(tokenSetSelect ? tokenSetSelect.value : '') + }) + } + function applyLivePreview(name, value) { + // The custom property is written immediately, NOT deferred: this is + // what repaints the real components on the page, and the browser + // already batches it into the next frame for free. Only the rich + // preview — which has to READ the cascade back — is worth delaying. if (value.trim() === '') { document.documentElement.style.removeProperty(name) } else { document.documentElement.style.setProperty(name, value) } - // Reflect the live token edit into the rich preview. - updatePreview(tokenSetSelect ? tokenSetSelect.value : '') + schedulePreviewRepaint() } function markDirty(name, value, container) { diff --git a/l10n/be.js b/l10n/be.js index a0e0059b..4c70da54 100644 --- a/l10n/be.js +++ b/l10n/be.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/be.json b/l10n/be.json index d5c6d9a7..83e807e5 100644 --- a/l10n/be.json +++ b/l10n/be.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/bg.js b/l10n/bg.js index 42a09e58..6be17136 100644 --- a/l10n/bg.js +++ b/l10n/bg.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/bg.json b/l10n/bg.json index 5af0b94c..31e89a03 100644 --- a/l10n/bg.json +++ b/l10n/bg.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/bs.js b/l10n/bs.js index 900dc608..9f626be9 100644 --- a/l10n/bs.js +++ b/l10n/bs.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/bs.json b/l10n/bs.json index 3cbe5a5c..29031306 100644 --- a/l10n/bs.json +++ b/l10n/bs.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/ca.js b/l10n/ca.js index 67604e4b..a9b3c351 100644 --- a/l10n/ca.js +++ b/l10n/ca.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/ca.json b/l10n/ca.json index f993cc4a..a36e6656 100644 --- a/l10n/ca.json +++ b/l10n/ca.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/cs.js b/l10n/cs.js index 76090022..4b6d4c1e 100644 --- a/l10n/cs.js +++ b/l10n/cs.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/cs.json b/l10n/cs.json index 65668085..e7b3bdd7 100644 --- a/l10n/cs.json +++ b/l10n/cs.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/da.js b/l10n/da.js index 98172251..cc66de2d 100644 --- a/l10n/da.js +++ b/l10n/da.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/da.json b/l10n/da.json index 5d0116ac..d00f1a47 100644 --- a/l10n/da.json +++ b/l10n/da.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/de.js b/l10n/de.js index 23f2b42c..c7bab4ff 100644 --- a/l10n/de.js +++ b/l10n/de.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/de.json b/l10n/de.json index 1c26a1da..fbbadb11 100644 --- a/l10n/de.json +++ b/l10n/de.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/el.js b/l10n/el.js index 337b88cc..de4ab318 100644 --- a/l10n/el.js +++ b/l10n/el.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/el.json b/l10n/el.json index e2f8091d..edc18971 100644 --- a/l10n/el.json +++ b/l10n/el.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/en.js b/l10n/en.js index bcecd569..5601be38 100644 --- a/l10n/en.js +++ b/l10n/en.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/en.json b/l10n/en.json index 5ab80e58..59d37b63 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/es.js b/l10n/es.js index ed872d04..ad6e419d 100644 --- a/l10n/es.js +++ b/l10n/es.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/es.json b/l10n/es.json index 7127c888..dcc0de2c 100644 --- a/l10n/es.json +++ b/l10n/es.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/et.js b/l10n/et.js index ebd52d8d..b72402b8 100644 --- a/l10n/et.js +++ b/l10n/et.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/et.json b/l10n/et.json index 850e09a2..e15f4ef9 100644 --- a/l10n/et.json +++ b/l10n/et.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/fi.js b/l10n/fi.js index 19230389..eacab9f3 100644 --- a/l10n/fi.js +++ b/l10n/fi.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/fi.json b/l10n/fi.json index 7efd765e..16d6f803 100644 --- a/l10n/fi.json +++ b/l10n/fi.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/fr.js b/l10n/fr.js index 3eec4bba..94282ed2 100644 --- a/l10n/fr.js +++ b/l10n/fr.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/fr.json b/l10n/fr.json index db94af33..8c8e09b5 100644 --- a/l10n/fr.json +++ b/l10n/fr.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/ga.js b/l10n/ga.js index ba3f45a7..76246542 100644 --- a/l10n/ga.js +++ b/l10n/ga.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/ga.json b/l10n/ga.json index 304ae816..d2ffd0ac 100644 --- a/l10n/ga.json +++ b/l10n/ga.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/hr.js b/l10n/hr.js index ac3124a9..a84a980b 100644 --- a/l10n/hr.js +++ b/l10n/hr.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/hr.json b/l10n/hr.json index 76544066..b46f4f35 100644 --- a/l10n/hr.json +++ b/l10n/hr.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/hu.js b/l10n/hu.js index 6396a458..f9845b4b 100644 --- a/l10n/hu.js +++ b/l10n/hu.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/hu.json b/l10n/hu.json index eb08e8f8..6ebd8978 100644 --- a/l10n/hu.json +++ b/l10n/hu.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/is.js b/l10n/is.js index a85ff730..2a66205c 100644 --- a/l10n/is.js +++ b/l10n/is.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/is.json b/l10n/is.json index 04128c27..9bd40117 100644 --- a/l10n/is.json +++ b/l10n/is.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/it.js b/l10n/it.js index 82215ce7..86b40291 100644 --- a/l10n/it.js +++ b/l10n/it.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/it.json b/l10n/it.json index d9f9965a..59efa1c7 100644 --- a/l10n/it.json +++ b/l10n/it.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/lb.js b/l10n/lb.js index 4a99d73f..d819bfee 100644 --- a/l10n/lb.js +++ b/l10n/lb.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/lb.json b/l10n/lb.json index 7af80aa0..6e544398 100644 --- a/l10n/lb.json +++ b/l10n/lb.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/lt.js b/l10n/lt.js index 9e479da8..5cb121b1 100644 --- a/l10n/lt.js +++ b/l10n/lt.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/lt.json b/l10n/lt.json index 7ec03609..e6bd2300 100644 --- a/l10n/lt.json +++ b/l10n/lt.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/lv.js b/l10n/lv.js index b0faea25..bb905928 100644 --- a/l10n/lv.js +++ b/l10n/lv.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/lv.json b/l10n/lv.json index 95af4e4a..d65271ca 100644 --- a/l10n/lv.json +++ b/l10n/lv.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/mk.js b/l10n/mk.js index 0eb9ba72..73420f84 100644 --- a/l10n/mk.js +++ b/l10n/mk.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/mk.json b/l10n/mk.json index b4f53de9..38f11a67 100644 --- a/l10n/mk.json +++ b/l10n/mk.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/mt.js b/l10n/mt.js index 2ca240ed..db9b9a64 100644 --- a/l10n/mt.js +++ b/l10n/mt.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/mt.json b/l10n/mt.json index 6c047f1d..d91b0199 100644 --- a/l10n/mt.json +++ b/l10n/mt.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/nb.js b/l10n/nb.js index 9362800d..4b676b4b 100644 --- a/l10n/nb.js +++ b/l10n/nb.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/nb.json b/l10n/nb.json index c3e0fd25..1fb49c22 100644 --- a/l10n/nb.json +++ b/l10n/nb.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/nl.js b/l10n/nl.js index 0b4a0456..759bec2c 100644 --- a/l10n/nl.js +++ b/l10n/nl.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Niets", "Changed": "Gewijzigd", "+%n more": "+nog %n", - "Apps, files, messages, and more": "Apps, bestanden, berichten en meer" + "Apps, files, messages, and more": "Apps, bestanden, berichten en meer", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/nl.json b/l10n/nl.json index e6ebec7c..725b1ade 100644 --- a/l10n/nl.json +++ b/l10n/nl.json @@ -356,7 +356,10 @@ "Nothing": "Niets", "Changed": "Gewijzigd", "+%n more": "+nog %n", - "Apps, files, messages, and more": "Apps, bestanden, berichten en meer" + "Apps, files, messages, and more": "Apps, bestanden, berichten en meer", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/pl.js b/l10n/pl.js index fe368b92..d36a86d3 100644 --- a/l10n/pl.js +++ b/l10n/pl.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/pl.json b/l10n/pl.json index ab23c5eb..bcf3050a 100644 --- a/l10n/pl.json +++ b/l10n/pl.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/pt.js b/l10n/pt.js index 7d23d4ec..9bedc1bd 100644 --- a/l10n/pt.js +++ b/l10n/pt.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/pt.json b/l10n/pt.json index f4c48245..0d271289 100644 --- a/l10n/pt.json +++ b/l10n/pt.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/rm.js b/l10n/rm.js index 0551dc06..6de22df6 100644 --- a/l10n/rm.js +++ b/l10n/rm.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/rm.json b/l10n/rm.json index 88b457f1..1b93e6bd 100644 --- a/l10n/rm.json +++ b/l10n/rm.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/ro.js b/l10n/ro.js index 7b13bb5d..141e7afa 100644 --- a/l10n/ro.js +++ b/l10n/ro.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/ro.json b/l10n/ro.json index c300263b..e33f3610 100644 --- a/l10n/ro.json +++ b/l10n/ro.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/ru.js b/l10n/ru.js index f35a6eda..954a0cbf 100644 --- a/l10n/ru.js +++ b/l10n/ru.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/ru.json b/l10n/ru.json index c865621d..3704c5e9 100644 --- a/l10n/ru.json +++ b/l10n/ru.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/sk.js b/l10n/sk.js index 6e6185e5..8bff230c 100644 --- a/l10n/sk.js +++ b/l10n/sk.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/sk.json b/l10n/sk.json index d4d1aa70..e5357082 100644 --- a/l10n/sk.json +++ b/l10n/sk.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/sl.js b/l10n/sl.js index 3db7eea8..0c3e5ef0 100644 --- a/l10n/sl.js +++ b/l10n/sl.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/sl.json b/l10n/sl.json index 3b03e48b..7d12ce13 100644 --- a/l10n/sl.json +++ b/l10n/sl.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/sq.js b/l10n/sq.js index 4098c64b..6feeb1e4 100644 --- a/l10n/sq.js +++ b/l10n/sq.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/sq.json b/l10n/sq.json index 4a3e2061..8f78491b 100644 --- a/l10n/sq.json +++ b/l10n/sq.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/sr.js b/l10n/sr.js index d7b7d04a..af16313f 100644 --- a/l10n/sr.js +++ b/l10n/sr.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/sr.json b/l10n/sr.json index d2032f19..d4dfca52 100644 --- a/l10n/sr.json +++ b/l10n/sr.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/sv.js b/l10n/sv.js index 088d95f8..2e2a4e0a 100644 --- a/l10n/sv.js +++ b/l10n/sv.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/sv.json b/l10n/sv.json index 991d5450..e71294bd 100644 --- a/l10n/sv.json +++ b/l10n/sv.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/tr.js b/l10n/tr.js index 59829bde..afcb0ff5 100644 --- a/l10n/tr.js +++ b/l10n/tr.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/tr.json b/l10n/tr.json index 70524af4..49534aa7 100644 --- a/l10n/tr.json +++ b/l10n/tr.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/l10n/uk.js b/l10n/uk.js index 5c4d472f..31aade71 100644 --- a/l10n/uk.js +++ b/l10n/uk.js @@ -357,7 +357,10 @@ OC.L10N.register( "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "nplurals=2; plural=(n != 1);" ) diff --git a/l10n/uk.json b/l10n/uk.json index 0ed1d12d..8e2ec4b8 100644 --- a/l10n/uk.json +++ b/l10n/uk.json @@ -356,7 +356,10 @@ "Nothing": "Nothing", "Changed": "Changed", "+%n more": "+%n more", - "Apps, files, messages, and more": "Apps, files, messages, and more" + "Apps, files, messages, and more": "Apps, files, messages, and more", + "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", + "Let the primary colour drive every component": "Let the primary colour drive every component", + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." }, "plurals": {} } diff --git a/templates/settings/admin.php b/templates/settings/admin.php index 2261c6b2..349a9436 100644 --- a/templates/settings/admin.php +++ b/templates/settings/admin.php @@ -414,6 +414,27 @@ class="checkbox"
+ +
+ checked> + +

+ t('While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.')); ?> +

+
+ - + +
+ t('Home')); ?> + › + t('Orders')); ?> + › + 2026 +
-
t('Orders')); ?>
+
t('Orders')); ?>
+ - + - - + +
t('Name')); ?>t('Date')); ?>t('Status')); ?>
+ +
+ + + +
-
t('Dialog')); ?>
+
t('Dialog')); ?>12
- - + +
+
t('Heading')); ?>
+
t('Body text of a page, with')); ?> t('a link')); ?>.
+
+ t('Muted')); ?> + t('Light')); ?> + t('Lighter')); ?> +
+
+ t('Error')); ?> + t('Warning')); ?> + t('Success')); ?> +
+
+ +
+ + + + +
+ - + - +
+ + @@ -278,11 +337,11 @@ class="checkbox" diff --git a/tests/e2e/spec-coverage/token-editor-edit-behavior.spec.ts b/tests/e2e/spec-coverage/token-editor-edit-behavior.spec.ts index dd5a3a35..cf11fdbf 100644 --- a/tests/e2e/spec-coverage/token-editor-edit-behavior.spec.ts +++ b/tests/e2e/spec-coverage/token-editor-edit-behavior.spec.ts @@ -58,8 +58,10 @@ test.describe('token-editor-edit-behavior', () => { await page.waitForSelector('#nldesign-token-editor', { timeout: 15_000 }) await dismissSyncDialog(page) - // Pick the first colour text field in the editor and read its token name. - const field = page.locator('.nldesign-color-text').first() + // Pick the first EDITABLE colour text field and read its token name. + // Nextcloud's base tokens lead the list and stay locked until the + // admin opts in, so the first field overall is a disabled one. + const field = page.locator('.nldesign-color-text:not([disabled])').first() await expect(field).toBeAttached() const tokenName = await field.getAttribute('data-token') expect(tokenName, 'colour field must carry a data-token').toBeTruthy() @@ -75,13 +77,19 @@ test.describe('token-editor-edit-behavior', () => { await field.fill(NEW) await field.dispatchEvent('input') - // Live preview: the inline style on the document element must now carry - // the new value for this token (applyLivePreview → documentElement.style). - const inlineVal = await page.evaluate( - (name) => document.documentElement.style.getPropertyValue(name).trim(), + // Live preview: the preview carries the new value for this token, and + // the page does not — an unsaved edit is a preview until it is saved + // (applyLivePreview → #nldesign-preview.style, never ). + const inline = await page.evaluate( + (name) => ({ + preview: (document.getElementById('nldesign-preview') as HTMLElement) + .style.getPropertyValue(name).trim(), + page: document.documentElement.style.getPropertyValue(name).trim(), + }), tokenName as string, ) - expect(inlineVal.toLowerCase()).toBe(NEW) + expect(inline.preview.toLowerCase()).toBe(NEW) + expect(inline.page).toBe('') // A custom-value badge must appear on the edited row (markDirty). await expect(badge).toHaveCount(1) @@ -102,7 +110,7 @@ test.describe('token-editor-edit-behavior', () => { await page.waitForSelector('#nldesign-token-editor', { timeout: 15_000 }) await dismissSyncDialog(page) - const field = page.locator('.nldesign-color-text').first() + const field = page.locator('.nldesign-color-text:not([disabled])').first() const tokenName = await field.getAttribute('data-token') const picker = page.locator( `.nldesign-color-picker[data-token="${tokenName}"]`, @@ -125,7 +133,7 @@ test.describe('token-editor-edit-behavior', () => { await page.waitForSelector('#nldesign-token-editor', { timeout: 15_000 }) await dismissSyncDialog(page) - const field = page.locator('.nldesign-color-text').first() + const field = page.locator('.nldesign-color-text:not([disabled])').first() const tokenName = await field.getAttribute('data-token') const row = page.locator(`[data-token-row="${tokenName}"]`) const badge = row.locator('.nldesign-token-custom-badge') @@ -140,10 +148,10 @@ test.describe('token-editor-edit-behavior', () => { await expect(resetBtn).toBeVisible() await resetBtn.click() - // Reset removes the inline style (document.documentElement.style.removeProperty) - // and removes the custom badge. + // Reset removes the preview's inline style and removes the custom badge. const inlineVal = await page.evaluate( - (name) => document.documentElement.style.getPropertyValue(name).trim(), + (name) => (document.getElementById('nldesign-preview') as HTMLElement) + .style.getPropertyValue(name).trim(), tokenName as string, ) expect(inlineVal).toBe('') diff --git a/tests/e2e/workflows/token-apply-persistence.workflow.spec.ts b/tests/e2e/workflows/token-apply-persistence.workflow.spec.ts index 3e6667e6..e9e781f7 100644 --- a/tests/e2e/workflows/token-apply-persistence.workflow.spec.ts +++ b/tests/e2e/workflows/token-apply-persistence.workflow.spec.ts @@ -11,7 +11,8 @@ * the GET /settings/overrides response now contain it), and * - after a FRESH RELOAD the editor shows the persisted override (input value * + custom badge) — i.e. the override survives a round-trip, and - * - resetting / clearing the override removes it from the backend. + * - clearing the override removes it from the backend. (The row's reset + * button goes back to the SAVED value, so it is not the way to drop one.) * * It also probes whether the saved override is REFLECTED in the live CSS * variable on the page (the "applies the theme" promise). See the fixme block — @@ -22,7 +23,7 @@ * theming). The prior file content is snapshotted in beforeAll and RESTORED in * afterAll so the dev instance is never left re-themed. */ -import { test, expect } from '@playwright/test' +import { test, expect, type Page } from '@playwright/test' import { openTheming, requestToken, @@ -39,6 +40,20 @@ const TEST_VALUE = '#a1b2c3' // deliberate, easily-recognised, easily-reverted v let baselineOverrides: Record = {} +/** + * Unlock Nextcloud's base tokens, which --color-primary is one of: they stay + * locked until the admin ticks the opt-in and confirms its warning. + * + * @param page The Playwright page, on the theming settings. + */ +async function unlockBaseTokens(page: Page): Promise { + await page.locator('#nldesign-base-unlock').click() + await page + .locator('#nldesign-base-unlock-overlay .nldesign-dialog-confirm') + .click() + await expect(page.locator('#nldesign-base-unlock')).toBeChecked() +} + test.describe('workflow: token-apply persistence', () => { test.describe.configure({ mode: 'serial', timeout: 90_000 }) @@ -69,6 +84,7 @@ test.describe('workflow: token-apply persistence', () => { const token = await requestToken(page) // Drive the real editor UI: type the value into the --color-primary text field. + await unlockBaseTokens(page) const textField = page.locator( `.nldesign-color-text[data-token="${TEST_TOKEN}"]`, ) @@ -149,7 +165,7 @@ test.describe('workflow: token-apply persistence', () => { expect(live.toLowerCase()).toBe(TEST_VALUE.toLowerCase()) }) - test('clearing the override REMOVES it from the backend (reset path)', async ({ + test('clearing the override REMOVES it from the backend', async ({ page, }) => { await openTheming(page) @@ -159,8 +175,12 @@ test.describe('workflow: token-apply persistence', () => { const before = await getOverrides(page, token) expect(before[TEST_TOKEN]).toBe(TEST_VALUE) - // Click the row's reset button (↺) to clear the custom value, then Save. - await page.locator(`.nldesign-reset-btn[data-token="${TEST_TOKEN}"]`).click() + // Empty the row's field, then Save: an emptied row is how a saved value + // stops being saved. The reset button would restore the saved value. + await unlockBaseTokens(page) + await page + .locator(`.nldesign-color-text[data-token="${TEST_TOKEN}"]`) + .fill('') await page.locator('#nldesign-save-btn').click() await expect(page.locator('#nldesign-save-status')).toHaveText('', { timeout: 10_000, @@ -170,7 +190,7 @@ test.describe('workflow: token-apply persistence', () => { const after = await getOverrides(page, token) expect( after[TEST_TOKEN], - 'override should be removed after reset+save', + 'override should be removed after clearing the field and saving', ).toBeUndefined() // The served file no longer contains the declaration. diff --git a/tests/vitest/playgroundSelection.spec.js b/tests/vitest/playgroundSelection.spec.js index dc5866d7..eafacd5a 100644 --- a/tests/vitest/playgroundSelection.spec.js +++ b/tests/vitest/playgroundSelection.spec.js @@ -117,6 +117,21 @@ describe('component instrument: the rows under a component', () => { ) }) + it('leads with the tokens every variant shares, apart from any one variant', () => { + // The note cards' corner used to sit among the info rows, where it read + // as belonging to the info card alone. + const notes = playground.componentById(inventory, 'note-cards') + const groups = playground.rowsByState(notes) + + expect(groups[0].state.id).toBe('all') + expect(groups[0].tokens.map((token) => token.name)).toEqual([ + '--nldesign-component-notecard-border-radius', + ]) + expect( + groups.slice(1).flatMap((group) => group.tokens.map((token) => token.name)), + ).not.toContain('--nldesign-component-notecard-border-radius') + }) + it('puts a token-less fact under its own state and nowhere else', () => { const groups = playground.rowsByState(component) const disabled = groups.find((group) => group.state.id === 'disabled') @@ -221,6 +236,43 @@ describe('component instrument: the token set export', () => { expect(parseTokens(result.css)).toEqual(tokens) }) + it('writes a component token the admin set by name', () => { + // The bug this guards: `sources` runs Nextcloud variable => token, and + // every override used to BE a Nextcloud variable. The component layer + // added rows named for the token itself, those names matched nothing in + // a map keyed by `--color-*`, and every one of them was reported as + // inexpressible and left out. An admin who set the header colours and + // saved the result as a theme got a theme with no header colours in it. + const result = playground.exportCss( + tokens, + { + '--nldesign-component-header-background-color': '#e8eff6', + '--nldesign-component-header-color': '#11304e', + }, + sources, + ) + const exported = parseTokens(result.css) + + expect(exported['--nldesign-component-header-background-color']).toBe( + '#e8eff6', + ) + expect(exported['--nldesign-component-header-color']).toBe('#11304e') + expect(result.unexpressed).toEqual([]) + }) + + it('lets a token set by name beat the same token reached through a variable', () => { + const result = playground.exportCss( + tokens, + { + '--color-primary': '#ff0000', + '--nldesign-color-primary': '#00ff00', + }, + sources, + ) + + expect(parseTokens(result.css)['--nldesign-color-primary']).toBe('#00ff00') + }) + it('keeps the defining variable when two overrides read one token', () => { // The map is many-to-one: `overrides.css` points both --color-primary // and --color-primary-element at --nldesign-color-primary, and @@ -656,6 +708,19 @@ describe('the shipped stylesheets, reached', () => { }) }) +describe('the link specimen', () => { + it('draws real links, so hover and focus are the browser\'s own', () => { + // A span has no hover or focus of its own: the stage told the admin to + // hover the link for its hover colour, and nothing happened. + const markup = playground.STAGES.link( + null, + inventory.components.find((entry) => entry.id === 'link'), + ) + expect(markup.match(//g)).toHaveLength(2) + expect(markup).not.toContain('') + }) +}) + describe('the login card, against the page it stands for', () => { // Transcribed from the rendered DOM of a real Nextcloud 34 login page. The // class names are the contract: they are what core's stylesheet, the @@ -687,6 +752,17 @@ describe('the login card, against the page it stands for', () => { expect(markup).toContain('class="button-vue__icon"') }) + it('puts the log-in button in the login button scope, and only that one', () => { + // On the real page `#body-login .button-vue--primary` does this; the + // specimen cannot carry that id, so it is named instead. Without it the + // card's button read the primary button's token, not the login one. + const submit = markup.slice(markup.indexOf('button-vue--vue-primary')) + expect(submit.slice(0, submit.indexOf('>'))).toContain( + 'data-thematiq-component="login-button"', + ) + expect(markup.split('data-thematiq-component="login-button"')).toHaveLength(2) + }) + it('gives a text button no icon span and an icon button no text span', () => { // NcButton's own `:empty` and `:has()` rules are what turn an icon-only // button square and close up a text-only one; emitting both spans From 91bd26f37a794774c87eef7cbacb829d4ccda641 Mon Sep 17 00:00:00 2001 From: Remko Date: Tue, 29 Sep 2026 09:36:32 +0200 Subject: [PATCH 64/72] style: format frontend sources with prettier --- css/admin.css | 449 ++++++++++++++---- css/playground.css | 34 +- css/systems/nldesign/element-overrides.css | 4 +- js/admin.js | 119 +++-- js/lib/tokenTransforms.js | 11 +- js/playground.js | 21 +- .../token-editor-edit-behavior.spec.ts | 13 +- .../token-apply-persistence.workflow.spec.ts | 4 +- tests/vitest/componentTokenReach.spec.js | 5 +- tests/vitest/playgroundInventory.spec.js | 4 +- tests/vitest/playgroundSelection.spec.js | 14 +- 11 files changed, 514 insertions(+), 164 deletions(-) diff --git a/css/admin.css b/css/admin.css index dd5bd5cc..f63daa78 100644 --- a/css/admin.css +++ b/css/admin.css @@ -771,7 +771,10 @@ padding: 0 10px; /* The header reads its own component tokens first, the way #header does in systems/nldesign/theme.css, and the set summary only when they are unset. */ - background: var(--nldesign-component-header-background-color, var(--prev-header-bg)); + background: var( + --nldesign-component-header-background-color, + var(--prev-header-bg) + ); border-bottom: var(--prev-header-border); } .nl-mini__logo { @@ -833,7 +836,10 @@ display: flex; min-height: 0; margin: 5px; - border-radius: var(--nldesign-component-content-card-container-border-radius, var(--prev-radius-container)); + border-radius: var( + --nldesign-component-content-card-container-border-radius, + var(--prev-radius-container) + ); overflow: hidden; background: var(--prev-surface); } @@ -865,10 +871,16 @@ font-size: 8px; /* Both, because the entry is a `span`: nldesign's blanket text rule claims every span with `!important` and reads this property first. */ - --nldesign-color-on-surface: var(--nldesign-component-navigation-color, var(--prev-text)); + --nldesign-color-on-surface: var( + --nldesign-component-navigation-color, + var(--prev-text) + ); color: var(--nldesign-component-navigation-color, var(--prev-text)); padding: 4px 7px; - border-radius: var(--nldesign-component-navigation-border-radius, var(--prev-radius-pill)); + border-radius: var( + --nldesign-component-navigation-border-radius, + var(--prev-radius-pill) + ); white-space: nowrap; overflow: hidden; } @@ -881,7 +893,10 @@ flex: none; } .nl-mini__menuitem--active { - background: var(--nldesign-component-navigation-active-background-color, var(--prev-primary)); + background: var( + --nldesign-component-navigation-active-background-color, + var(--prev-primary) + ); --nldesign-color-on-surface: var(--prev-primary-text); color: var(--prev-primary-text); font-weight: 700; @@ -897,7 +912,8 @@ } .nl-mini__widget { background: var(--prev-surface); - border: 1px solid var(--nldesign-component-content-card-border-color, var(--prev-border)); + border: 1px solid + var(--nldesign-component-content-card-border-color, var(--prev-border)); border-radius: var(--prev-radius); overflow: hidden; height: 100%; @@ -919,22 +935,42 @@ table token. The labels are words rather than bars so the weight shows. */ .nl-mini__table thead th { padding: 4px 9px; - border-bottom: 1px solid var(--nldesign-component-table-border-color, var(--color-border, var(--prev-border))); - background-color: var(--nldesign-component-table-header-background-color, transparent); - color: var(--nldesign-component-table-header-color, var(--color-text-maxcontrast, var(--prev-muted))); + border-bottom: 1px solid + var( + --nldesign-component-table-border-color, + var(--color-border, var(--prev-border)) + ); + background-color: var( + --nldesign-component-table-header-background-color, + transparent + ); + color: var( + --nldesign-component-table-header-color, + var(--color-text-maxcontrast, var(--prev-muted)) + ); font-size: 7px; font-weight: var(--nldesign-component-table-header-font-weight, normal); text-align: left; } .nl-mini__table td { padding: 6px 9px; - border-bottom: 1px solid var(--nldesign-component-table-border-color, var(--color-border, var(--prev-border))); + border-bottom: 1px solid + var( + --nldesign-component-table-border-color, + var(--color-border, var(--prev-border)) + ); } .nl-mini__table tr.is-zebra td { - background: var(--nldesign-component-table-zebra-background-color, var(--color-background-dark, var(--prev-bg))); + background: var( + --nldesign-component-table-zebra-background-color, + var(--color-background-dark, var(--prev-bg)) + ); } .nl-mini__table tr.is-hover td { - background: var(--nldesign-component-table-row-hover-background-color, var(--color-background-hover, var(--prev-bg))); + background: var( + --nldesign-component-table-row-hover-background-color, + var(--color-background-hover, var(--prev-bg)) + ); } .nl-mini__table td::after { content: ''; @@ -969,7 +1005,8 @@ .nl-mini__sidebar { flex: 0 0 88px; background: var(--prev-surface); - border-left: 1px solid var(--nldesign-component-sidebar-border-color, var(--prev-border)); + border-left: 1px solid + var(--nldesign-component-sidebar-border-color, var(--prev-border)); padding: 8px; display: flex; flex-direction: column; @@ -997,7 +1034,8 @@ .nl-mini__sidebar-tabs { display: flex; gap: 6px; - border-bottom: 1px solid var(--nldesign-component-sidebar-divider-color, var(--prev-border)); + border-bottom: 1px solid + var(--nldesign-component-sidebar-divider-color, var(--prev-border)); } .nl-mini__sidebar-tab { width: 20px; @@ -1042,8 +1080,14 @@ min-width: 12px; padding: 0 3px; border-radius: var(--nldesign-component-badge-border-radius, 999px); - background: var(--nldesign-component-badge-background-color, var(--prev-primary)); - --nldesign-color-on-surface: var(--nldesign-component-badge-color, var(--prev-primary-text)); + background: var( + --nldesign-component-badge-background-color, + var(--prev-primary) + ); + --nldesign-color-on-surface: var( + --nldesign-component-badge-color, + var(--prev-primary-text) + ); color: var(--nldesign-component-badge-color, var(--prev-primary-text)); font-size: 7px; font-weight: 700; @@ -1052,8 +1096,14 @@ } .nl-mini__star { margin-left: 4px; - --nldesign-color-on-surface: var(--nldesign-component-badge-favorite-color, var(--color-favorite, #a08b00)); - color: var(--nldesign-component-badge-favorite-color, var(--color-favorite, #a08b00)); + --nldesign-color-on-surface: var( + --nldesign-component-badge-favorite-color, + var(--color-favorite, #a08b00) + ); + color: var( + --nldesign-component-badge-favorite-color, + var(--color-favorite, #a08b00) + ); } /* Note cards: the type colour is the fill, and the type text colour the @@ -1070,7 +1120,10 @@ gap: 4px; padding: 4px 5px; border-inline-start: 2px solid var(--nl-note-icon); - border-radius: var(--nldesign-component-notecard-border-radius, var(--border-radius, 3px)); + border-radius: var( + --nldesign-component-notecard-border-radius, + var(--border-radius, 3px) + ); background: var(--nl-note-fill); } .nl-mini__note-icon { @@ -1089,23 +1142,56 @@ } .nl-mini__note--info { --nl-note-fill: var(--nldesign-component-notecard-info-color, var(--color-info)); - --nl-note-icon: var(--nldesign-component-notecard-info-icon-color, var(--color-info-text, var(--color-element-info))); - --nl-note-text: var(--nldesign-component-notecard-info-text-color, var(--prev-text)); + --nl-note-icon: var( + --nldesign-component-notecard-info-icon-color, + var(--color-info-text, var(--color-element-info)) + ); + --nl-note-text: var( + --nldesign-component-notecard-info-text-color, + var(--prev-text) + ); } .nl-mini__note--warning { - --nl-note-fill: var(--nldesign-component-notecard-warning-color, var(--color-warning)); - --nl-note-icon: var(--nldesign-component-notecard-warning-icon-color, var(--color-warning-text, var(--color-element-warning))); - --nl-note-text: var(--nldesign-component-notecard-warning-text-color, var(--prev-text)); + --nl-note-fill: var( + --nldesign-component-notecard-warning-color, + var(--color-warning) + ); + --nl-note-icon: var( + --nldesign-component-notecard-warning-icon-color, + var(--color-warning-text, var(--color-element-warning)) + ); + --nl-note-text: var( + --nldesign-component-notecard-warning-text-color, + var(--prev-text) + ); } .nl-mini__note--error { - --nl-note-fill: var(--nldesign-component-notecard-error-color, var(--color-error)); - --nl-note-icon: var(--nldesign-component-notecard-error-icon-color, var(--color-error-text, var(--color-element-error))); - --nl-note-text: var(--nldesign-component-notecard-error-text-color, var(--prev-text)); + --nl-note-fill: var( + --nldesign-component-notecard-error-color, + var(--color-error) + ); + --nl-note-icon: var( + --nldesign-component-notecard-error-icon-color, + var(--color-error-text, var(--color-element-error)) + ); + --nl-note-text: var( + --nldesign-component-notecard-error-text-color, + var(--prev-text) + ); } .nl-mini__note--success { - --nl-note-fill: var(--nldesign-component-notecard-success-color, var(--color-success)); - --nl-note-icon: var(--nldesign-component-notecard-success-icon-color, var(--color-success-text, var(--color-element-success))); - --nl-note-text: var(--nldesign-component-notecard-success-text-color, var(--prev-text)); + --nl-note-fill: var( + --nldesign-component-notecard-success-color, + var(--color-success) + ); + --nl-note-icon: var( + --nldesign-component-notecard-success-icon-color, + var(--color-success-text, var(--color-element-success)) + ); + --nl-note-text: var( + --nldesign-component-notecard-success-text-color, + var(--prev-text) + ); } /* Toasts: a card on its own background with the type colour as a stripe, @@ -1124,7 +1210,10 @@ display: block; padding: 4px 5px; border-inline-start: 2px solid var(--nl-toast-stripe); - border-radius: var(--nldesign-component-toast-border-radius, var(--border-radius, 3px)); + border-radius: var( + --nldesign-component-toast-border-radius, + var(--border-radius, 3px) + ); background: var(--nl-toast-fill); box-shadow: 0 0 4px rgba(0, 0, 0, 0.2); } @@ -1136,22 +1225,34 @@ opacity: 0.7; } .nl-mini__toast--success { - --nl-toast-fill: var(--nldesign-component-toast-success-background-color, var(--prev-surface)); + --nl-toast-fill: var( + --nldesign-component-toast-success-background-color, + var(--prev-surface) + ); --nl-toast-text: var(--nldesign-component-toast-success-color, var(--prev-text)); --nl-toast-stripe: var(--color-element-success, var(--color-success)); } .nl-mini__toast--error { - --nl-toast-fill: var(--nldesign-component-toast-error-background-color, var(--prev-surface)); + --nl-toast-fill: var( + --nldesign-component-toast-error-background-color, + var(--prev-surface) + ); --nl-toast-text: var(--nldesign-component-toast-error-color, var(--prev-text)); --nl-toast-stripe: var(--color-element-error, var(--color-error)); } .nl-mini__toast--warning { - --nl-toast-fill: var(--nldesign-component-toast-warning-background-color, var(--prev-surface)); + --nl-toast-fill: var( + --nldesign-component-toast-warning-background-color, + var(--prev-surface) + ); --nl-toast-text: var(--nldesign-component-toast-warning-color, var(--prev-text)); --nl-toast-stripe: var(--color-element-warning, var(--color-warning)); } .nl-mini__toast--info { - --nl-toast-fill: var(--nldesign-component-toast-info-background-color, var(--prev-surface)); + --nl-toast-fill: var( + --nldesign-component-toast-info-background-color, + var(--prev-surface) + ); --nl-toast-text: var(--nldesign-component-toast-info-color, var(--prev-text)); --nl-toast-stripe: var(--color-element-info, var(--prev-primary)); } @@ -1164,7 +1265,10 @@ width: 5px; height: 5px; border-radius: 50%; - background: var(--nldesign-component-avatar-online-status-color, var(--color-success, #2d7b41)); + background: var( + --nldesign-component-avatar-online-status-color, + var(--color-success, #2d7b41) + ); } /* Breadcrumbs: a link crumb, a hovered one and the current one. They are @@ -1180,11 +1284,20 @@ .nl-mini__crumb { padding: 1px 5px; border-radius: var(--prev-radius-pill); - --nldesign-color-on-surface: var(--nldesign-component-breadcrumb-color, var(--color-primary, var(--prev-primary))); - color: var(--nldesign-component-breadcrumb-color, var(--color-primary, var(--prev-primary))); + --nldesign-color-on-surface: var( + --nldesign-component-breadcrumb-color, + var(--color-primary, var(--prev-primary)) + ); + color: var( + --nldesign-component-breadcrumb-color, + var(--color-primary, var(--prev-primary)) + ); } .nl-mini__crumb--hover { - background: var(--nldesign-component-breadcrumb-hover-background-color, var(--color-background-hover, var(--prev-bg))); + background: var( + --nldesign-component-breadcrumb-hover-background-color, + var(--color-background-hover, var(--prev-bg)) + ); } .nl-mini__crumb--current { --nldesign-color-on-surface: var(--prev-text); @@ -1192,8 +1305,14 @@ font-weight: 700; } .nl-mini__crumb-sep { - --nldesign-color-on-surface: var(--nldesign-component-breadcrumb-separator-color, var(--color-text-maxcontrast, var(--prev-muted))); - color: var(--nldesign-component-breadcrumb-separator-color, var(--color-text-maxcontrast, var(--prev-muted))); + --nldesign-color-on-surface: var( + --nldesign-component-breadcrumb-separator-color, + var(--color-text-maxcontrast, var(--prev-muted)) + ); + color: var( + --nldesign-component-breadcrumb-separator-color, + var(--color-text-maxcontrast, var(--prev-muted)) + ); } /* A form row: text field, checked checkbox, progress bar. */ @@ -1209,34 +1328,56 @@ align-items: center; height: 14px; padding: 0 6px; - border: 1px solid var(--nldesign-component-textbox-border-color, var(--color-border-maxcontrast, var(--prev-border))); - border-radius: var(--nldesign-component-textbox-border-radius, var(--prev-radius)); + border: 1px solid + var( + --nldesign-component-textbox-border-color, + var(--color-border-maxcontrast, var(--prev-border)) + ); + border-radius: var( + --nldesign-component-textbox-border-radius, + var(--prev-radius) + ); } .nl-mini__input-text { width: 50%; height: 4px; border-radius: 2px; - background: var(--nldesign-component-textbox-placeholder-color, var(--color-placeholder-dark, var(--prev-muted))); + background: var( + --nldesign-component-textbox-placeholder-color, + var(--color-placeholder-dark, var(--prev-muted)) + ); } .nl-mini__check { flex: none; width: 10px; height: 10px; - border-radius: var(--nldesign-component-checkbox-border-radius, var(--border-radius-small, 3px)); - background: var(--nldesign-component-checkbox-checked-background-color, var(--color-primary-element, var(--prev-primary))); + border-radius: var( + --nldesign-component-checkbox-border-radius, + var(--border-radius-small, 3px) + ); + background: var( + --nldesign-component-checkbox-checked-background-color, + var(--color-primary-element, var(--prev-primary)) + ); } .nl-mini__progress { flex: 0 0 50px; height: 5px; overflow: hidden; border-radius: var(--nldesign-component-progress-border-radius, 999px); - background: var(--nldesign-component-progress-track-background-color, var(--color-background-darker, var(--prev-border))); + background: var( + --nldesign-component-progress-track-background-color, + var(--color-background-darker, var(--prev-border)) + ); } .nl-mini__progress-fill { display: block; width: 60%; height: 100%; - background: var(--nldesign-component-progress-background-color, var(--color-primary-element, var(--prev-primary))); + background: var( + --nldesign-component-progress-background-color, + var(--color-primary-element, var(--prev-primary)) + ); } /* Typography in the dialog: each text painted from its own token. They are @@ -1247,30 +1388,66 @@ gap: 4px; } .nl-mini__type-heading { - --nldesign-color-on-surface: var(--nldesign-component-heading-color, var(--color-main-text, var(--prev-text))); - color: var(--nldesign-component-heading-color, var(--color-main-text, var(--prev-text))); - font-family: var(--nldesign-component-heading-font-family, var(--font-face, inherit)); - font-weight: var(--nldesign-component-heading-font-weight, var(--font-weight-heading, 700)); + --nldesign-color-on-surface: var( + --nldesign-component-heading-color, + var(--color-main-text, var(--prev-text)) + ); + color: var( + --nldesign-component-heading-color, + var(--color-main-text, var(--prev-text)) + ); + font-family: var( + --nldesign-component-heading-font-family, + var(--font-face, inherit) + ); + font-weight: var( + --nldesign-component-heading-font-weight, + var(--font-weight-heading, 700) + ); font-size: 13px; line-height: 1.2; } .nl-mini__type-paragraph { - --nldesign-color-on-surface: var(--nldesign-component-paragraph-color, var(--color-main-text, var(--prev-text))); - color: var(--nldesign-component-paragraph-color, var(--color-main-text, var(--prev-text))); - font-family: var(--nldesign-component-paragraph-font-family, var(--font-face, inherit)); - font-weight: var(--nldesign-component-paragraph-font-weight, var(--font-weight-default, 400)); + --nldesign-color-on-surface: var( + --nldesign-component-paragraph-color, + var(--color-main-text, var(--prev-text)) + ); + color: var( + --nldesign-component-paragraph-color, + var(--color-main-text, var(--prev-text)) + ); + font-family: var( + --nldesign-component-paragraph-font-family, + var(--font-face, inherit) + ); + font-weight: var( + --nldesign-component-paragraph-font-weight, + var(--font-weight-default, 400) + ); font-size: 9px; } .nl-mini__type-link { - --nldesign-color-on-surface: var(--nldesign-component-link-color, var(--color-primary, var(--prev-primary))); - color: var(--nldesign-component-link-color, var(--color-primary, var(--prev-primary))); + --nldesign-color-on-surface: var( + --nldesign-component-link-color, + var(--color-primary, var(--prev-primary)) + ); + color: var( + --nldesign-component-link-color, + var(--color-primary, var(--prev-primary)) + ); text-decoration: underline; cursor: pointer; } .nl-mini__type-link:hover, .nl-mini__type-link:focus-visible { - --nldesign-color-on-surface: var(--nldesign-component-link-hover-color, var(--color-primary-hover, var(--prev-primary))); - color: var(--nldesign-component-link-hover-color, var(--color-primary-hover, var(--prev-primary))); + --nldesign-color-on-surface: var( + --nldesign-component-link-hover-color, + var(--color-primary-hover, var(--prev-primary)) + ); + color: var( + --nldesign-component-link-hover-color, + var(--color-primary-hover, var(--prev-primary)) + ); } .nl-mini__type-row { display: flex; @@ -1278,28 +1455,64 @@ font-size: 9px; } .nl-mini__type-muted { - --nldesign-color-on-surface: var(--nldesign-component-text-muted-color, var(--color-text-maxcontrast, var(--prev-muted))); - color: var(--nldesign-component-text-muted-color, var(--color-text-maxcontrast, var(--prev-muted))); + --nldesign-color-on-surface: var( + --nldesign-component-text-muted-color, + var(--color-text-maxcontrast, var(--prev-muted)) + ); + color: var( + --nldesign-component-text-muted-color, + var(--color-text-maxcontrast, var(--prev-muted)) + ); } .nl-mini__type-light { - --nldesign-color-on-surface: var(--nldesign-component-text-light-color, var(--color-text-light, var(--prev-muted))); - color: var(--nldesign-component-text-light-color, var(--color-text-light, var(--prev-muted))); + --nldesign-color-on-surface: var( + --nldesign-component-text-light-color, + var(--color-text-light, var(--prev-muted)) + ); + color: var( + --nldesign-component-text-light-color, + var(--color-text-light, var(--prev-muted)) + ); } .nl-mini__type-lighter { - --nldesign-color-on-surface: var(--nldesign-component-text-lighter-color, var(--color-text-lighter, var(--prev-muted))); - color: var(--nldesign-component-text-lighter-color, var(--color-text-lighter, var(--prev-muted))); + --nldesign-color-on-surface: var( + --nldesign-component-text-lighter-color, + var(--color-text-lighter, var(--prev-muted)) + ); + color: var( + --nldesign-component-text-lighter-color, + var(--color-text-lighter, var(--prev-muted)) + ); } .nl-mini__type-error { - --nldesign-color-on-surface: var(--nldesign-component-text-error-color, var(--color-text-error, var(--prev-error))); - color: var(--nldesign-component-text-error-color, var(--color-text-error, var(--prev-error))); + --nldesign-color-on-surface: var( + --nldesign-component-text-error-color, + var(--color-text-error, var(--prev-error)) + ); + color: var( + --nldesign-component-text-error-color, + var(--color-text-error, var(--prev-error)) + ); } .nl-mini__type-warning { - --nldesign-color-on-surface: var(--nldesign-component-text-warning-color, var(--color-text-warning, var(--prev-warning))); - color: var(--nldesign-component-text-warning-color, var(--color-text-warning, var(--prev-warning))); + --nldesign-color-on-surface: var( + --nldesign-component-text-warning-color, + var(--color-text-warning, var(--prev-warning)) + ); + color: var( + --nldesign-component-text-warning-color, + var(--color-text-warning, var(--prev-warning)) + ); } .nl-mini__type-success { - --nldesign-color-on-surface: var(--nldesign-component-text-success-color, var(--color-text-success, #2d7b41)); - color: var(--nldesign-component-text-success-color, var(--color-text-success, #2d7b41)); + --nldesign-color-on-surface: var( + --nldesign-component-text-success-color, + var(--color-text-success, #2d7b41) + ); + color: var( + --nldesign-component-text-success-color, + var(--color-text-success, #2d7b41) + ); } /* Each tab's full view shows that tab's part of the shell. js/playground.js @@ -1331,7 +1544,10 @@ .nl-mini__modal { width: 78%; background: var(--prev-surface); - border-radius: var(--nldesign-component-dialog-border-radius, var(--border-radius-large)); + border-radius: var( + --nldesign-component-dialog-border-radius, + var(--border-radius-large) + ); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3); padding: 12px; } @@ -1423,7 +1639,10 @@ which is what makes a pale fill readable. White on every fill assumed a saturated one and lost the label on a set with pale status colours. */ #nldesign-preview .nl-btn--error { - background: var(--nldesign-component-button-error-background-color, var(--prev-error)); + background: var( + --nldesign-component-button-error-background-color, + var(--prev-error) + ); border-color: var( --nldesign-component-button-error-hover-background-color, var(--color-error-hover, var(--prev-error)) @@ -1441,7 +1660,10 @@ --nldesign-component-button-success-hover-background-color, var(--color-success-hover, var(--color-success, #2d7b41)) ); - color: var(--nldesign-component-button-success-color, var(--color-success-text, #fff)); + color: var( + --nldesign-component-button-success-color, + var(--color-success-text, #fff) + ); } #nldesign-preview .nl-btn--success:hover { background: var( @@ -1456,11 +1678,17 @@ #nldesign-preview .nl-btn--primary:hover { background: var( --nldesign-component-button-primary-action-hover-background-color, - var(--nldesign-component-button-primary-action-background-color, var(--prev-primary)) + var( + --nldesign-component-button-primary-action-background-color, + var(--prev-primary) + ) ); border-color: var( --nldesign-component-button-primary-action-hover-background-color, - var(--nldesign-component-button-primary-action-background-color, var(--prev-primary)) + var( + --nldesign-component-button-primary-action-background-color, + var(--prev-primary) + ) ); } #nldesign-preview .nl-btn--secondary:hover { @@ -1472,13 +1700,20 @@ #nldesign-preview .nl-btn--tertiary:hover { background: var( --nldesign-component-button-hover-background-color, - color-mix(in srgb, var(--color-primary-element, var(--prev-primary)) 8%, transparent) + color-mix( + in srgb, + var(--color-primary-element, var(--prev-primary)) 8%, + transparent + ) ); } #nldesign-preview .nl-btn--tertiary:active { background: var( --nldesign-component-button-active-background-color, - var(--color-primary-element-light, color-mix(in srgb, var(--prev-primary) 20%, transparent)) + var( + --color-primary-element-light, + color-mix(in srgb, var(--prev-primary) 20%, transparent) + ) ); } #nldesign-preview .nl-btn--error:hover { @@ -1519,7 +1754,10 @@ width: 190px; background: var(--prev-surface); border: 1px solid var(--nldesign-component-login-box-border-color, transparent); - border-radius: var(--nldesign-component-login-box-border-radius, var(--border-radius-large)); + border-radius: var( + --nldesign-component-login-box-border-radius, + var(--border-radius-large) + ); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25); padding: 16px; display: flex; @@ -1533,15 +1771,25 @@ align-items: center; height: 16px; padding: 0 7px; - border-radius: var(--nldesign-component-textbox-border-radius, var(--prev-radius)); + border-radius: var( + --nldesign-component-textbox-border-radius, + var(--prev-radius) + ); background: var(--prev-surface); - border: 1px solid var(--nldesign-component-textbox-border-color, var(--color-border-maxcontrast, var(--prev-border))); + border: 1px solid + var( + --nldesign-component-textbox-border-color, + var(--color-border-maxcontrast, var(--prev-border)) + ); } .nl-login__placeholder { width: 55%; height: 4px; border-radius: 2px; - background: var(--nldesign-component-textbox-placeholder-color, var(--color-placeholder-dark, var(--prev-muted))); + background: var( + --nldesign-component-textbox-placeholder-color, + var(--color-placeholder-dark, var(--prev-muted)) + ); } /* The login button: its own tokens first, the primary button's behind them, the way #body-login resolves it (css/component-scopes.css). Declared after @@ -1552,15 +1800,24 @@ padding: 7px; background: var( --nldesign-component-login-button-background-color, - var(--nldesign-component-button-primary-action-background-color, var(--prev-primary)) + var( + --nldesign-component-button-primary-action-background-color, + var(--prev-primary) + ) ); border-color: var( --nldesign-component-login-button-background-color, - var(--nldesign-component-button-primary-action-background-color, var(--prev-primary)) + var( + --nldesign-component-button-primary-action-background-color, + var(--prev-primary) + ) ); color: var( --nldesign-component-login-button-color, - var(--nldesign-component-button-primary-action-color, var(--prev-primary-text)) + var( + --nldesign-component-button-primary-action-color, + var(--prev-primary-text) + ) ); border-radius: var( --nldesign-component-login-button-border-radius, @@ -1572,23 +1829,35 @@ --nldesign-component-login-button-hover-background-color, var( --nldesign-component-button-primary-action-hover-background-color, - var(--nldesign-component-login-button-background-color, var(--prev-primary)) + var( + --nldesign-component-login-button-background-color, + var(--prev-primary) + ) ) ); border-color: var( --nldesign-component-login-button-hover-background-color, var( --nldesign-component-button-primary-action-hover-background-color, - var(--nldesign-component-login-button-background-color, var(--prev-primary)) + var( + --nldesign-component-login-button-background-color, + var(--prev-primary) + ) ) ); } /* The slogan in its own colour and typeface. It is a `div`, so it also sets the on-surface property nldesign's blanket text rule reads first. */ .nl-login__slogan { - --nldesign-color-on-surface: var(--nldesign-component-logo-slogan-color, var(--prev-primary-text)); + --nldesign-color-on-surface: var( + --nldesign-component-logo-slogan-color, + var(--prev-primary-text) + ); color: var(--nldesign-component-logo-slogan-color, var(--prev-primary-text)); - font-family: var(--nldesign-component-logo-slogan-font-family, var(--font-face, inherit)); + font-family: var( + --nldesign-component-logo-slogan-font-family, + var(--font-face, inherit) + ); font-size: 9px; } diff --git a/css/playground.css b/css/playground.css index 6f92ac38..b59c2c5f 100644 --- a/css/playground.css +++ b/css/playground.css @@ -318,11 +318,17 @@ #nldesign-preview .nldesign-pg-input.nldesign-pg-textarea:empty::before { content: attr(data-placeholder); - color: var(--nldesign-component-textarea-placeholder-color, var(--color-text-maxcontrast)); + color: var( + --nldesign-component-textarea-placeholder-color, + var(--color-text-maxcontrast) + ); } #nldesign-preview .nldesign-pg-input.nldesign-pg-textarea:focus { - border-color: var(--nldesign-component-textarea-focus-border-color, var(--color-main-text)); + border-color: var( + --nldesign-component-textarea-focus-border-color, + var(--color-main-text) + ); } #nldesign-preview .nldesign-pg-input.nldesign-pg-textarea.is-invalid { @@ -1063,7 +1069,10 @@ #nldesign-preview .nldesign-pg-table th { padding: 10px 12px; border-bottom: 1px solid var(--color-border); - background-color: var(--nldesign-component-table-header-background-color, transparent); + background-color: var( + --nldesign-component-table-header-background-color, + transparent + ); color: var(--color-text-maxcontrast); font-size: 13px; font-weight: var(--nldesign-component-table-header-font-weight, 400); @@ -1614,7 +1623,10 @@ #nldesign-preview .nldesign-pg-option:hover, #nldesign-preview .nldesign-pg-option.is-hover { background: var(--vs-dropdown-option--active-bg, var(--color-background-hover)); - color: var(--vs-dropdown-option--active-color, var(--vs-dropdown-color, var(--color-main-text))); + color: var( + --vs-dropdown-option--active-color, + var(--vs-dropdown-color, var(--color-main-text)) + ); } /* Checkbox, radio, switch ------------------------------------------------------------ */ @@ -1778,17 +1790,20 @@ } #nldesign-preview .nldesign-pg-note.is-warning { - border-inline-start: 4px solid var(--color-warning-text, var(--color-element-warning)); + border-inline-start: 4px solid + var(--color-warning-text, var(--color-element-warning)); background: var(--color-warning); } #nldesign-preview .nldesign-pg-note.is-error { - border-inline-start: 4px solid var(--color-error-text, var(--color-element-error)); + border-inline-start: 4px solid + var(--color-error-text, var(--color-element-error)); background: var(--color-error); } #nldesign-preview .nldesign-pg-note.is-success { - border-inline-start: 4px solid var(--color-success-text, var(--color-element-success)); + border-inline-start: 4px solid + var(--color-success-text, var(--color-element-success)); background: var(--color-success); } @@ -1883,7 +1898,10 @@ max-width: 380px; margin-bottom: 8px; padding: 12px 14px; - border-radius: var(--nldesign-component-toast-border-radius, var(--border-radius)); + border-radius: var( + --nldesign-component-toast-border-radius, + var(--border-radius) + ); box-shadow: 0 0 6px 0 var(--color-box-shadow); font-size: 14px; } diff --git a/css/systems/nldesign/element-overrides.css b/css/systems/nldesign/element-overrides.css index a38d5b03..dab23c99 100644 --- a/css/systems/nldesign/element-overrides.css +++ b/css/systems/nldesign/element-overrides.css @@ -499,7 +499,9 @@ a:not(#header a):not(.tile-widget a):not(.tile-widget * a) { */ .button-vue--vue-secondary, .button-vue--secondary { - --nldesign-color-on-surface: var(--nldesign-component-button-secondary-action-color); + --nldesign-color-on-surface: var( + --nldesign-component-button-secondary-action-color + ); } /* diff --git a/js/admin.js b/js/admin.js index 24bb8511..a18c6d9b 100644 --- a/js/admin.js +++ b/js/admin.js @@ -378,7 +378,9 @@ // design system has a file of its own, every other set the shared one. var option = tokenSetSelect !== null - ? tokenSetSelect.querySelector('option[value="' + tokenSetId + '"]') + ? tokenSetSelect.querySelector( + 'option[value="' + tokenSetId + '"]', + ) : null var designSystem = option !== null @@ -386,10 +388,14 @@ : 'nldesign' var file = designSystem === 'none' || tokenSetId === STOCK_TOKEN_SET - ? 'custom-overrides-' + tokenSetId.toLowerCase().replace(/[^a-z0-9-]/g, '') + ? 'custom-overrides-' + + tokenSetId.toLowerCase().replace(/[^a-z0-9-]/g, '') : 'custom-overrides' var href = OC.filePath('thematiq', 'css', file + '.css') - if (LayerSwap.pathnameOf(link.getAttribute('href')) === LayerSwap.pathnameOf(href)) { + if ( + LayerSwap.pathnameOf(link.getAttribute('href')) + === LayerSwap.pathnameOf(href) + ) { return } @@ -1347,7 +1353,10 @@ var captured = Boolean( tokenSetData.theming && tokenSetData.theming.captured === true, ) - if ((tokenSetData.design_system || 'nldesign') === 'none' && captured === false) { + if ( + (tokenSetData.design_system || 'nldesign') === 'none' + && captured === false + ) { var resetDiffs = [] if (currentTheming.primary_color) { resetDiffs.push({ @@ -1525,7 +1534,8 @@ }) } else if ( mode === 'default' - && (removedNow === true || currentTheming.has_custom_background === true) + && (removedNow === true + || currentTheming.has_custom_background === true) ) { diffs.push({ label: t('thematiq', 'Background image'), @@ -2743,7 +2753,10 @@ + '' + '' @@ -3004,7 +3017,10 @@ * @return {string} The token value. */ function pickerValue(picker) { - if (picker.dataset.format === 'rgb' && typeof TT.hexToRgbTriplet === 'function') { + if ( + picker.dataset.format === 'rgb' + && typeof TT.hexToRgbTriplet === 'function' + ) { return TT.hexToRgbTriplet(picker.value) || picker.value } return picker.value @@ -3122,8 +3138,15 @@ } // A saved value stays on the preview; with none, the // preview falls back to what the theme itself declares. - if (typeof state.custom === 'string' && state.custom !== '') { - previewTarget().style.setProperty(name, state.custom, 'important') + if ( + typeof state.custom === 'string' + && state.custom !== '' + ) { + previewTarget().style.setProperty( + name, + state.custom, + 'important', + ) } else { previewTarget().style.removeProperty(name) } @@ -3498,9 +3521,7 @@ overlay .querySelector('.nldesign-dialog-confirm') .addEventListener('click', function () { - var nameEl = document.getElementById( - 'nldesign-save-newset-name', - ) + var nameEl = document.getElementById('nldesign-save-newset-name') if (nameEl === null) { return } @@ -3542,9 +3563,7 @@ * @return {void} */ function showError(message) { - var errorEl = document.getElementById( - 'nldesign-save-newset-error', - ) + var errorEl = document.getElementById('nldesign-save-newset-error') var nameEl = document.getElementById('nldesign-save-newset-name') if (errorEl === null) { return @@ -3847,8 +3866,10 @@ // NL Design System. tokenSetsData[id] = { id: id, name: name, design_system: designSystem } - if (tokenSetSelect !== null - && tokenSetSelect.querySelector('option[value="' + id + '"]') === null + if ( + tokenSetSelect !== null + && tokenSetSelect.querySelector('option[value="' + id + '"]') + === null ) { var option = document.createElement('option') option.value = id @@ -4037,7 +4058,8 @@ // reset button returns to it, and no row is unsaved. Object.keys(tokenEditorState).forEach(function (k) { var state = tokenEditorState[k] - state.custom = overrides[k] !== undefined ? overrides[k] : null + state.custom = + overrides[k] !== undefined ? overrides[k] : null state.saved = state.current state.isDirty = false }) @@ -4451,35 +4473,38 @@ newTokenSetId === STOCK_TOKEN_SET ? Promise.resolve({ status: 'ok' }) : fetch(overridesUrl('', newTokenSetId), { - headers: { requesttoken: OC.requestToken }, - }) - .then(function (r) { - return r.json() + headers: { requesttoken: OC.requestToken }, }) - .then(function (existingData) { - var merged = Object.assign( - {}, - existingData.overrides || {}, - toApply, - ) - return fetch( - OC.generateUrl('/apps/thematiq/settings/overrides'), - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - requesttoken: OC.requestToken, + .then(function (r) { + return r.json() + }) + .then(function (existingData) { + var merged = Object.assign( + {}, + existingData.overrides || {}, + toApply, + ) + return fetch( + OC.generateUrl( + '/apps/thematiq/settings/overrides', + ), + { + method: 'POST', + headers: { + 'Content-Type': + 'application/json', + requesttoken: OC.requestToken, + }, + body: JSON.stringify({ + overrides: merged, + tokenSet: newTokenSetId, + }), }, - body: JSON.stringify({ - overrides: merged, - tokenSet: newTokenSetId, - }), - }, - ) - }) - .then(function (r) { - return r.json() - }) + ) + }) + .then(function (r) { + return r.json() + }) pinned .then(function (saveData) { @@ -5745,7 +5770,9 @@ .catch(function (err) { resetThemeBtn.disabled = false console.error('Error resetting the theme:', err) - notify(t('thematiq', 'The theme could not be reset.')) + notify( + t('thematiq', 'The theme could not be reset.'), + ) }) }, true, diff --git a/js/lib/tokenTransforms.js b/js/lib/tokenTransforms.js index 58634d1f..2d5e75d6 100644 --- a/js/lib/tokenTransforms.js +++ b/js/lib/tokenTransforms.js @@ -109,7 +109,9 @@ + triplet .slice(1) .map(function (channel) { - return ('0' + Math.min(255, Number(channel)).toString(16)).slice(-2) + return ( + '0' + Math.min(255, Number(channel)).toString(16) + ).slice(-2) }) .join('') ) @@ -130,7 +132,12 @@ */ function hexToRgbTriplet(value) { var hex = normaliseColorForPicker(value) - if (hex === null || value === undefined || value === null || String(value).trim() === '') { + if ( + hex === null + || value === undefined + || value === null + || String(value).trim() === '' + ) { return null } return [1, 3, 5] diff --git a/js/playground.js b/js/playground.js index 6666b526..264d3184 100644 --- a/js/playground.js +++ b/js/playground.js @@ -1019,7 +1019,10 @@ // exist on Nextcloud 32 alone — is offered while that version is // the one drawn, and nowhere else: a row that moves nothing on // the instance being themed is a control that does nothing. - if (spec.until !== undefined && versionOf(state, component) > spec.until) { + if ( + spec.until !== undefined + && versionOf(state, component) > spec.until + ) { return } filtered.appendChild(cloneRow(state, spec)) @@ -1793,9 +1796,11 @@ * @return {boolean} True when it has version notes, or is drawn per version. */ function hasVersions(component) { - return (component.versionNotes || []).length > 0 + return ( + (component.versionNotes || []).length > 0 || component.id === 'header-bar' || component.versioned === true + ) } /** @@ -1825,7 +1830,13 @@ notes.forEach(function (note) { if (note.versions.indexOf(version) !== -1) { - lines.push(t('thematiq', 'Nextcloud {version}:', { version: String(version) }) + ' ' + note.text) + lines.push( + t('thematiq', 'Nextcloud {version}:', { + version: String(version), + }) + + ' ' + + note.text, + ) } }) @@ -2724,7 +2735,9 @@ + '" data-placeholder="' + attr(t('thematiq', 'Describe the change in a few lines…')) + '">' - + (invalid ? t('thematiq', 'A longer explanation, over several lines.') : '') + + (invalid + ? t('thematiq', 'A longer explanation, over several lines.') + : '') + '' ) }, diff --git a/tests/e2e/spec-coverage/token-editor-edit-behavior.spec.ts b/tests/e2e/spec-coverage/token-editor-edit-behavior.spec.ts index cf11fdbf..5d5b9af1 100644 --- a/tests/e2e/spec-coverage/token-editor-edit-behavior.spec.ts +++ b/tests/e2e/spec-coverage/token-editor-edit-behavior.spec.ts @@ -82,8 +82,11 @@ test.describe('token-editor-edit-behavior', () => { // (applyLivePreview → #nldesign-preview.style, never ). const inline = await page.evaluate( (name) => ({ - preview: (document.getElementById('nldesign-preview') as HTMLElement) - .style.getPropertyValue(name).trim(), + preview: ( + document.getElementById('nldesign-preview') as HTMLElement + ).style + .getPropertyValue(name) + .trim(), page: document.documentElement.style.getPropertyValue(name).trim(), }), tokenName as string, @@ -150,8 +153,10 @@ test.describe('token-editor-edit-behavior', () => { // Reset removes the preview's inline style and removes the custom badge. const inlineVal = await page.evaluate( - (name) => (document.getElementById('nldesign-preview') as HTMLElement) - .style.getPropertyValue(name).trim(), + (name) => + (document.getElementById('nldesign-preview') as HTMLElement).style + .getPropertyValue(name) + .trim(), tokenName as string, ) expect(inlineVal).toBe('') diff --git a/tests/e2e/workflows/token-apply-persistence.workflow.spec.ts b/tests/e2e/workflows/token-apply-persistence.workflow.spec.ts index e9e781f7..88e9f9c8 100644 --- a/tests/e2e/workflows/token-apply-persistence.workflow.spec.ts +++ b/tests/e2e/workflows/token-apply-persistence.workflow.spec.ts @@ -165,9 +165,7 @@ test.describe('workflow: token-apply persistence', () => { expect(live.toLowerCase()).toBe(TEST_VALUE.toLowerCase()) }) - test('clearing the override REMOVES it from the backend', async ({ - page, - }) => { + test('clearing the override REMOVES it from the backend', async ({ page }) => { await openTheming(page) const token = await requestToken(page) diff --git a/tests/vitest/componentTokenReach.spec.js b/tests/vitest/componentTokenReach.spec.js index ef6b8111..77a4d1c9 100644 --- a/tests/vitest/componentTokenReach.spec.js +++ b/tests/vitest/componentTokenReach.spec.js @@ -364,7 +364,10 @@ describe('component tokens: the capture block is taken on body', () => { const head = css.slice(0, open) const start = Math.max(head.lastIndexOf('}') + 1, head.lastIndexOf('*/') + 2) - const selectors = head.slice(start).split(',').map((s) => s.trim()) + const selectors = head + .slice(start) + .split(',') + .map((s) => s.trim()) expect(selectors[0]).toBe('body') // The token editor's preview is captured too: an unsaved edit is diff --git a/tests/vitest/playgroundInventory.spec.js b/tests/vitest/playgroundInventory.spec.js index 87784a57..ec3cea11 100644 --- a/tests/vitest/playgroundInventory.spec.js +++ b/tests/vitest/playgroundInventory.spec.js @@ -174,7 +174,9 @@ describe('component inventory: the tokens', () => { const dangling = inventory.components.flatMap((component) => { // Callout 0 is the shared group: a token every state reads, such // as the note cards' corner, listed ahead of the states. - const states = new Set([0].concat(component.states.map((state) => state.n))) + const states = new Set( + [0].concat(component.states.map((state) => state.n)), + ) return component.tokens .concat(component.fixed) .filter((entry) => states.has(entry.callout) === false) diff --git a/tests/vitest/playgroundSelection.spec.js b/tests/vitest/playgroundSelection.spec.js index eafacd5a..23fe482e 100644 --- a/tests/vitest/playgroundSelection.spec.js +++ b/tests/vitest/playgroundSelection.spec.js @@ -128,7 +128,9 @@ describe('component instrument: the rows under a component', () => { '--nldesign-component-notecard-border-radius', ]) expect( - groups.slice(1).flatMap((group) => group.tokens.map((token) => token.name)), + groups + .slice(1) + .flatMap((group) => group.tokens.map((token) => token.name)), ).not.toContain('--nldesign-component-notecard-border-radius') }) @@ -709,14 +711,16 @@ describe('the shipped stylesheets, reached', () => { }) describe('the link specimen', () => { - it('draws real links, so hover and focus are the browser\'s own', () => { + it("draws real links, so hover and focus are the browser's own", () => { // A span has no hover or focus of its own: the stage told the admin to // hover the link for its hover colour, and nothing happened. const markup = playground.STAGES.link( null, inventory.components.find((entry) => entry.id === 'link'), ) - expect(markup.match(//g)).toHaveLength(2) + expect(markup.match(//g)).toHaveLength( + 2, + ) expect(markup).not.toContain('') }) }) @@ -760,7 +764,9 @@ describe('the login card, against the page it stands for', () => { expect(submit.slice(0, submit.indexOf('>'))).toContain( 'data-thematiq-component="login-button"', ) - expect(markup.split('data-thematiq-component="login-button"')).toHaveLength(2) + expect(markup.split('data-thematiq-component="login-button"')).toHaveLength( + 2, + ) }) it('gives a text button no icon span and an icon button no text span', () => { From 34b5671e60ee925c7a6dd1a393681f40bb0058f8 Mon Sep 17 00:00:00 2001 From: Remko Date: Tue, 29 Sep 2026 09:36:32 +0200 Subject: [PATCH 65/72] fix(l10n): add the new editor and playground strings to en.json --- l10n/en.json | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/l10n/en.json b/l10n/en.json index 59d37b63..6c1fa53b 100644 --- a/l10n/en.json +++ b/l10n/en.json @@ -359,7 +359,43 @@ "Apps, files, messages, and more": "Apps, files, messages, and more", "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.": "The primary colour drives this component. Switch off \"Let the primary colour drive every component\" to set it separately.", "Let the primary colour drive every component": "Let the primary colour drive every component", - "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back." + "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.": "While this is on, the brand primary overrules any colour set on an individual component, and those controls are locked. Switching it off gives each component its own colour back.", + "A Nextcloud base colour, read far beyond any one component. Tick \"Also edit Nextcloud's base tokens\" to change it.": "A Nextcloud base colour, read far beyond any one component. Tick \"Also edit Nextcloud's base tokens\" to change it.", + "Also edit Nextcloud's base tokens": "Also edit Nextcloud's base tokens", + "Describe the change in a few lines…": "Describe the change in a few lines…", + "Do not ask again": "Do not ask again", + "Don't ask before saving while a token set is active": "Don't ask before saving while a token set is active", + "Don't ask before saving while the stock Nextcloud theme is active": "Don't ask before saving while the stock Nextcloud theme is active", + "e.g. j.jansen": "e.g. j.jansen", + "Edit Nextcloud's base tokens?": "Edit Nextcloud's base tokens?", + "Edit them anyway": "Edit them anyway", + "Favicon": "Favicon", + "Give the new theme a name.": "Give the new theme a name.", + "Keep these changes as a new theme?": "Keep these changes as a new theme?", + "My organisation": "My organisation", + "Name for the new theme": "Name for the new theme", + "Navigation bar logo": "Navigation bar logo", + "Nextcloud {version}:": "Nextcloud {version}:", + "No, change the Nextcloud theme": "No, change the Nextcloud theme", + "On Nextcloud 32 and 33 the app icons are images, so \"Header glyphs\" does not recolour them: they stay white. The other header glyphs do follow it. From Nextcloud 34 the app menu follows it too.": "On Nextcloud 32 and 33 the app icons are images, so \"Header glyphs\" does not recolour them: they stay white. The other header glyphs do follow it. From Nextcloud 34 the app menu follows it too.", + "Removed (plain colour)": "Removed (plain colour)", + "Reset theme to Nextcloud": "Reset theme to Nextcloud", + "Save these overrides?": "Save these overrides?", + "The new theme could not be created (HTTP {status}).": "The new theme could not be created (HTTP {status}).", + "The new theme could not be created.": "The new theme could not be created.", + "The preview keeps the styles of Nextcloud {running}, the version this instance runs.": "The preview keeps the styles of Nextcloud {running}, the version this instance runs.", + "The theme could not be reset.": "The theme could not be reset.", + "The theme exporter is not loaded on this page, so no new theme was created.": "The theme exporter is not loaded on this page, so no new theme was created.", + "Theme \"{name}\" created and applied.": "Theme \"{name}\" created and applied.", + "Theme \"{name}\" created and set as the active theme. Reload the page to see it.": "Theme \"{name}\" created and set as the active theme. Reload the page to see it.", + "Theme \"{name}\" was created but could not be applied. Pick it in the theme dropdown.": "Theme \"{name}\" was created but could not be applied. Pick it in the theme dropdown.", + "These are the colours and sizes Nextcloud itself is built from, such as the main text colour. Changing one changes every part of Nextcloud that reads it, which is far more than the component you are looking at, and not only what this panel shows. To change one component, use its own rows instead.": "These are the colours and sizes Nextcloud itself is built from, such as the main text colour. Changing one changes every part of Nextcloud that reads it, which is far more than the component you are looking at, and not only what this panel shows. To change one component, use its own rows instead.", + "These values could not be written into a theme and are kept as overrides instead: {names}": "These values could not be written into a theme and are kept as overrides instead: {names}", + "They are applied on top of {set}.": "They are applied on top of {set}.", + "This fully resets the theme to stock Nextcloud: the overrides of the current theme and every change written over the Nextcloud theme are deleted, and the Nextcloud theming colours and logo return to their defaults. Custom token sets, fonts and custom CSS are kept. This cannot be undone.": "This fully resets the theme to stock Nextcloud: the overrides of the current theme and every change written over the Nextcloud theme are deleted, and the Nextcloud theming colours and logo return to their defaults. Custom token sets, fonts and custom CSS are kept. This cannot be undone.", + "Token editor": "Token editor", + "Yes, save as a new theme": "Yes, save as a new theme", + "You are working on the stock Nextcloud theme. Save the changes as a token set of their own, or write them over the Nextcloud theme itself. Writing over it changes Nextcloud for everyone; \"Reset theme to Nextcloud\" brings the original back.": "You are working on the stock Nextcloud theme. Save the changes as a token set of their own, or write them over the Nextcloud theme itself. Writing over it changes Nextcloud for everyone; \"Reset theme to Nextcloud\" brings the original back." }, "plurals": {} } From d246ab55c6e44421ec79899821503f92d07c0983 Mon Sep 17 00:00:00 2001 From: Remko Date: Tue, 29 Sep 2026 09:40:07 +0200 Subject: [PATCH 66/72] fix(css): stop emitting duplicate selectors and empty rules in the stylesheets --- css/component-scopes.css | 24 --------- css/playground.css | 10 ++-- scripts/generate-component-scopes.mjs | 73 +++++++++++++++++++-------- 3 files changed, 55 insertions(+), 52 deletions(-) diff --git a/css/component-scopes.css b/css/component-scopes.css index 54fc6766..0b66cd65 100644 --- a/css/component-scopes.css +++ b/css/component-scopes.css @@ -277,9 +277,6 @@ table, th { background-color: var(--nldesign-component-table-header-background-color, transparent); -} - -th { font-weight: var(--nldesign-component-table-header-font-weight, normal); } @@ -378,11 +375,6 @@ th { } /* avatar */ -.avatardiv, -.user-status-icon, -[data-thematiq-component='avatar'] { -} - .user-status-icon { --user-status-color-online: var(--nldesign-component-avatar-online-status-color, #2D7B41) !important; } @@ -856,10 +848,6 @@ body .notecard.success { body [class*='_toast_success_'], body .toastify.toast-success { background-color: var(--nldesign-component-toast-success-background-color, var(--color-main-background)); -} - -body [class*='_toast_success_'], -body .toastify.toast-success { --thematiq-contrast-toast-success-color: lch(from var(--nldesign-component-toast-success-background-color) calc((49.44 - l) * infinity) 0 0); color: var(--nldesign-component-toast-success-color, var(--thematiq-contrast-toast-success-color, var(--color-main-text))); } @@ -867,10 +855,6 @@ body .toastify.toast-success { body [class*='_toast_error_'], body .toastify.toast-error { background-color: var(--nldesign-component-toast-error-background-color, var(--color-main-background)); -} - -body [class*='_toast_error_'], -body .toastify.toast-error { --thematiq-contrast-toast-error-color: lch(from var(--nldesign-component-toast-error-background-color) calc((49.44 - l) * infinity) 0 0); color: var(--nldesign-component-toast-error-color, var(--thematiq-contrast-toast-error-color, var(--color-main-text))); } @@ -878,10 +862,6 @@ body .toastify.toast-error { body [class*='_toast_warning_'], body .toastify.toast-warning { background-color: var(--nldesign-component-toast-warning-background-color, var(--color-main-background)); -} - -body [class*='_toast_warning_'], -body .toastify.toast-warning { --thematiq-contrast-toast-warning-color: lch(from var(--nldesign-component-toast-warning-background-color) calc((49.44 - l) * infinity) 0 0); color: var(--nldesign-component-toast-warning-color, var(--thematiq-contrast-toast-warning-color, var(--color-main-text))); } @@ -889,10 +869,6 @@ body .toastify.toast-warning { body [class*='_toast_info_'], body .toastify.toast-info { background-color: var(--nldesign-component-toast-info-background-color, var(--color-main-background)); -} - -body [class*='_toast_info_'], -body .toastify.toast-info { --thematiq-contrast-toast-info-color: lch(from var(--nldesign-component-toast-info-background-color) calc((49.44 - l) * infinity) 0 0); color: var(--nldesign-component-toast-info-color, var(--thematiq-contrast-toast-info-color, var(--color-main-text))); } diff --git a/css/playground.css b/css/playground.css index b59c2c5f..68aaed15 100644 --- a/css/playground.css +++ b/css/playground.css @@ -852,18 +852,16 @@ margin-block: 0 auto; } -/* Login.vue: `.login-box { width: 320px }` — "same size as dashboard panels". */ -#nldesign-preview .nldesign-pg-logincard { - width: 320px; - max-width: 100%; -} +/* Login.vue: `.login-box { width: 320px }` — "same size as dashboard panels". -/* The card's edge, painted from its token the way css/component-scopes.css + The card's edge, painted from its token the way css/component-scopes.css paints the real login box: Nextcloud's guest box draws no border, so there is no variable to redirect. Unset, the declaration is invalid and the card has no edge, as in Nextcloud. `!important` because the nldesign theme sets `border: none !important` on every guest box. */ #nldesign-preview .nldesign-pg-logincard { + width: 320px; + max-width: 100%; border: 1px solid var(--nldesign-component-login-box-border-color) !important; } diff --git a/scripts/generate-component-scopes.mjs b/scripts/generate-component-scopes.mjs index 67d0e3b2..e9502d9e 100644 --- a/scripts/generate-component-scopes.mjs +++ b/scripts/generate-component-scopes.mjs @@ -303,18 +303,16 @@ function contrastName(name) { } /** - * The rule lines one paint emits. + * The declaration lines one paint emits. * * @param {string} name The token being painted from. * @param {Object} token Its mapping entry. - * @param {Array} selectors The component's selectors. * - * @return {Array} The rule, one line per entry. + * @return {Array} The declarations, one line per entry. */ -function paintRule(name, token, selectors) { +function paintDeclarations(name, token) { const paint = token.paint const important = paint.important === true ? ' !important' : '' - const lines = [(paint.selectors || selectors).join(',\n') + ' {'] // A `template` wraps the token in the rest of a shorthand, for a property // one colour cannot fill on its own — `1px solid {token}` for an edge @@ -322,11 +320,10 @@ function paintRule(name, token, selectors) { // declaration is invalid at computed-value time, so the property takes // its initial value, which is exactly "no edge". if (paint.template !== undefined) { - lines.push('\t' + paint.property + ': ' + paint.template.replace('{token}', 'var(' + name + ')') + important + ';') - lines.push('}', '') - return lines + return ['\t' + paint.property + ': ' + paint.template.replace('{token}', 'var(' + name + ')') + important + ';'] } + const lines = [] let fallback = paint.fallback if (paint.contrastWith !== undefined) { @@ -338,34 +335,66 @@ function paintRule(name, token, selectors) { } lines.push('\t' + paint.property + ': var(' + name + ', ' + fallback + ')' + important + ';') - lines.push('}', '') + + return lines +} + +/** + * A component's paints as rules, one per selector list. + * + * Paints that land on the same selectors — a toast type's background and its + * text — share a rule, so no selector is written twice. + * + * @param {Object} component The component's mapping entry. + * + * @return {Array} The rules, one line per entry. + */ +function paintRules(component) { + const rules = new Map() + for (const [name, token] of paintsOf(component)) { + const selectors = (token.paint.selectors || component.selectors).join(',\n') + if (rules.has(selectors) === false) { + rules.set(selectors, []) + } + rules.get(selectors).push(...paintDeclarations(name, token)) + } + + const lines = [] + for (const [selectors, declarations] of rules) { + lines.push(selectors + ' {', ...declarations, '}', '') + } return lines } for (const [id, component] of Object.entries(mapping.components)) { - scopes.push('/* ' + id + ' */') - scopes.push(component.selectors.concat(specimenSelector(id)).join(',\n') + ' {') + const declarations = [] for (const [name, token] of Object.entries(component.tokens)) { if (token.paint !== undefined) { continue } - scopes.push('\t' + token.global + ': var(') - scopes.push('\t\t' + name + ',') - scopes.push('\t\tvar(' + captureName(token.global) + ')') - scopes.push('\t);') + declarations.push('\t' + token.global + ': var(') + declarations.push('\t\t' + name + ',') + declarations.push('\t\tvar(' + captureName(token.global) + ')') + declarations.push('\t);') } for (const [target, source] of Object.entries(aliasesOf(component))) { - scopes.push('\t' + target + ': var(') - scopes.push('\t\t' + source + ',') - scopes.push('\t\tvar(' + aliasCaptureName(target) + ')') - scopes.push('\t);') + declarations.push('\t' + target + ': var(') + declarations.push('\t\t' + source + ',') + declarations.push('\t\tvar(' + aliasCaptureName(target) + ')') + declarations.push('\t);') } - scopes.push('}', '') - for (const [name, token] of paintsOf(component)) { - scopes.push(...paintRule(name, token, component.selectors)) + scopes.push('/* ' + id + ' */') + // A component whose every token is a paint redirects nothing, and an empty + // rule would only be noise. + if (declarations.length > 0) { + scopes.push(component.selectors.concat(specimenSelector(id)).join(',\n') + ' {') + scopes.push(...declarations) + scopes.push('}', '') } + + scopes.push(...paintRules(component)) } const scopesOutput = scopes.join('\n') From 7e98f9dfd42c0997fa45f814a3c009b4f5d865b2 Mon Sep 17 00:00:00 2001 From: Remko Date: Tue, 29 Sep 2026 09:42:15 +0200 Subject: [PATCH 67/72] test(themes): cover the per-set overrides file and the branding capture 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. --- .../CustomTokenSetControllerCaptureTest.php | 218 ++++++++++++++++++ .../OverridesControllerCaptureTest.php | 133 +++++++++++ .../Service/BrandingCaptureServiceTest.php | 27 +++ .../CustomOverridesServicePerSetTest.php | 140 +++++++++++ tests/Unit/Service/ThemingServiceTest.php | 50 ++++ .../TokenSetServiceCapturedThemingTest.php | 132 +++++++++++ 6 files changed, 700 insertions(+) create mode 100644 tests/Unit/Controller/CustomTokenSetControllerCaptureTest.php create mode 100644 tests/Unit/Controller/OverridesControllerCaptureTest.php create mode 100644 tests/Unit/Service/CustomOverridesServicePerSetTest.php create mode 100644 tests/Unit/Service/TokenSetServiceCapturedThemingTest.php diff --git a/tests/Unit/Controller/CustomTokenSetControllerCaptureTest.php b/tests/Unit/Controller/CustomTokenSetControllerCaptureTest.php new file mode 100644 index 00000000..15a9b024 --- /dev/null +++ b/tests/Unit/Controller/CustomTokenSetControllerCaptureTest.php @@ -0,0 +1,218 @@ + value. + * + * @var array + */ + private array $appConfig = []; + + /** + * The request mock. + * + * @var IRequest&\PHPUnit\Framework\MockObject\MockObject + */ + private $request; + + /** + * The branding capture mock. + * + * @var BrandingCaptureService&\PHPUnit\Framework\MockObject\MockObject + */ + private $capture; + + /** + * The controller under test. + * + * @var CustomTokenSetController + */ + private CustomTokenSetController $controller; + + protected function setUp(): void { + parent::setUp(); + + $this->appDir = sys_get_temp_dir() . '/thematiq-capture-controller-' . uniqid(); + mkdir($this->appDir . '/css/tokens', 0777, true); + + $appManager = $this->createMock(IAppManager::class); + $appManager->method('getAppPath')->willReturn($this->appDir); + + $config = $this->createMock(IConfig::class); + $config->method('getAppValue')->willReturnCallback( + fn (string $app, string $key, $default = '') => ($this->appConfig[$key] ?? $default) + ); + $config->method('setAppValue')->willReturnCallback( + function (string $app, string $key, $value): void { + $this->appConfig[$key] = $value; + } + ); + + $service = new CustomTokenSetService( + $appManager, + $config, + new CustomTokenSetValidator(), + new ContrastService(), + new DarkPaletteService(new ContrastService(), new CssParserService(), $appManager, $this->createMock(LoggerInterface::class)) + ); + + $l = $this->createMock(IL10N::class); + $l->method('t')->willReturnArgument(0); + + $this->request = $this->createMock(IRequest::class); + $this->capture = $this->createMock(BrandingCaptureService::class); + + $this->controller = new CustomTokenSetController( + 'thematiq', + $this->request, + $service, + new CustomTokenSetValidator(), + new CssParserService(), + $l, + $this->createMock(ThemingAuditService::class), + $config, + $this->createMock(TokenSetConverterService::class), + $this->createMock(ThemingService::class), + $this->createMock(DesignSystemService::class), + $this->capture + ); + }//end setUp() + + protected function tearDown(): void { + $this->rrmdir($this->appDir); + parent::tearDown(); + }//end tearDown() + + /** + * Recursively remove a directory tree. + * + * @param string $dir The directory to remove. + * + * @return void + */ + private function rrmdir(string $dir): void { + if (is_dir($dir) === false) { + return; + } + + foreach (scandir($dir) as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + + $path = $dir . '/' . $entry; + if (is_dir($path) === true) { + $this->rrmdir($path); + } else { + unlink($path); + } + } + + rmdir($dir); + }//end rrmdir() + + /** + * Save a theme from the editor: a raw token set, capture asked for or not. + * + * @param bool $capture Whether `captureTheming` is sent. + * + * @return void + */ + private function savingFromTheEditor(bool $capture): void { + $params = [ + 'name' => 'OpenWoo', + 'content' => ":root {\n --nldesign-color-primary: #154273;\n}\n", + 'raw' => true, + 'captureTheming' => $capture, + ]; + $this->request->method('getParam')->willReturnCallback( + fn (string $key, $default = null) => ($params[$key] ?? $default) + ); + $this->request->method('getUploadedFile')->willReturn(null); + }//end savingFromTheEditor() + + /** + * The new theme gets the branding, and the editor gets it back. + */ + public function testASavedThemeCapturesTheBranding(): void { + $this->savingFromTheEditor(capture: true); + $this->capture->expects($this->once()) + ->method('capture') + ->with('custom-openwoo') + ->willReturn(['captured' => true, 'primary_color' => '#23845c']); + + $response = $this->controller->upload(); + + $this->assertSame(200, $response->getStatus()); + $this->assertSame('custom-openwoo', $response->getData()['id']); + $this->assertSame(['captured' => true, 'primary_color' => '#23845c'], $response->getData()['theming']); + }//end testASavedThemeCapturesTheBranding() + + /** + * Without the flag nothing is captured. + */ + public function testNoCaptureWithoutTheFlag(): void { + $this->savingFromTheEditor(capture: false); + $this->capture->expects($this->never())->method('capture'); + + $response = $this->controller->upload(); + + $this->assertArrayNotHasKey('theming', $response->getData()); + }//end testNoCaptureWithoutTheFlag() + + /** + * Deleting a custom theme forgets the branding it captured. + */ + public function testDeletingAThemeForgetsItsBranding(): void { + $this->savingFromTheEditor(capture: false); + $this->controller->upload(); + + $this->capture->expects($this->once())->method('forget')->with('custom-openwoo'); + + $response = $this->controller->delete(id: 'custom-openwoo'); + + $this->assertSame(200, $response->getStatus()); + }//end testDeletingAThemeForgetsItsBranding() +}//end class diff --git a/tests/Unit/Controller/OverridesControllerCaptureTest.php b/tests/Unit/Controller/OverridesControllerCaptureTest.php new file mode 100644 index 00000000..25bca7f5 --- /dev/null +++ b/tests/Unit/Controller/OverridesControllerCaptureTest.php @@ -0,0 +1,133 @@ +request = $this->createMock(IRequest::class); + $this->config = $this->createMock(IConfig::class); + $this->capture = $this->createMock(BrandingCaptureService::class); + + $this->controller = new OverridesController( + 'thematiq', + $this->request, + $this->createMock(CustomOverridesService::class), + new CssParserService(), + $this->createMock(ThemingAuditService::class), + $this->config, + $this->createMock(ThemingService::class), + $this->capture + ); + }//end setUp() + + /** + * Point the request at a set, asking for the branding to be captured. + * + * @param string $tokenSet The `tokenSet` parameter ('' for none). + * + * @return void + */ + private function saving(string $tokenSet): void { + $this->request->method('getParams')->willReturn(['overrides' => ['--color-primary' => '#112233']]); + $this->request->method('getParam')->willReturnMap( + [ + ['tokenSet', '', $tokenSet], + ['captureTheming', false, true], + ] + ); + }//end saving() + + /** + * The theme being saved gets the branding, and the editor gets it back. + */ + public function testCapturesIntoTheSavedTheme(): void { + $this->saving(tokenSet: 'custom-openwoo'); + $this->capture->expects($this->once()) + ->method('capture') + ->with('custom-openwoo') + ->willReturn(['captured' => true, 'primary_color' => '#23845c']); + + $response = $this->controller->setOverrides(); + + $this->assertSame(200, $response->getStatus()); + $this->assertSame(['captured' => true, 'primary_color' => '#23845c'], $response->getData()['theming']); + }//end testCapturesIntoTheSavedTheme() + + /** + * The stock set never captures: its branding is Nextcloud's own. + */ + public function testTheStockSetIsNeverCaptured(): void { + $this->saving(tokenSet: 'nextcloud'); + $this->capture->expects($this->never())->method('capture'); + + $response = $this->controller->setOverrides(); + + $this->assertArrayNotHasKey('theming', $response->getData()); + }//end testTheStockSetIsNeverCaptured() + + /** + * Without a set named, the instance's active set is the one saved. + */ + public function testNoSetNamedCapturesIntoTheActiveSet(): void { + $this->saving(tokenSet: ''); + $this->config->method('getAppValue')->willReturn('amsterdam'); + $this->capture->expects($this->once())->method('capture')->with('amsterdam')->willReturn(['captured' => true]); + + $this->controller->setOverrides(); + }//end testNoSetNamedCapturesIntoTheActiveSet() +}//end class diff --git a/tests/Unit/Service/BrandingCaptureServiceTest.php b/tests/Unit/Service/BrandingCaptureServiceTest.php index 36c1ee0a..4de20d3a 100644 --- a/tests/Unit/Service/BrandingCaptureServiceTest.php +++ b/tests/Unit/Service/BrandingCaptureServiceTest.php @@ -214,6 +214,33 @@ public function testCopiesEveryUploadedImage(): void { $this->assertSame($theming['logo'], $this->values['thematiq/synced_logo']); }//end testCopiesEveryUploadedImage() + /** + * An image of a type core cannot take back, or one that cannot be read, + * is left out rather than failing the save it rides on. + */ + public function testAnImageThatCannotComeBackIsLeftOut(): void { + $this->values['theming/logoMime'] = 'image/tiff'; + $this->values['theming/faviconMime'] = 'image/png'; + $this->imageManager->method('hasImage')->willReturnCallback( + fn (string $key): bool => in_array($key, ['logo', 'favicon'], true) + ); + $this->imageManager->method('getImage')->willThrowException(new \RuntimeException('unreadable')); + + $theming = $this->service->capture(setId: 'custom-openwoo'); + + $this->assertArrayNotHasKey('logo', $theming); + $this->assertArrayNotHasKey('favicon', $theming); + }//end testAnImageThatCannotComeBackIsLeftOut() + + /** + * An unreadable store reads as nothing captured. + */ + public function testAnUnreadableStoreReadsAsEmpty(): void { + $this->values['thematiq/' . BrandingCaptureService::CAPTURED_KEY] = 'not json'; + + $this->assertSame([], $this->service->all()); + }//end testAnUnreadableStoreReadsAsEmpty() + /** * Forgetting a set removes its captured branding and its copied images. */ diff --git a/tests/Unit/Service/CustomOverridesServicePerSetTest.php b/tests/Unit/Service/CustomOverridesServicePerSetTest.php new file mode 100644 index 00000000..1cfaff51 --- /dev/null +++ b/tests/Unit/Service/CustomOverridesServicePerSetTest.php @@ -0,0 +1,140 @@ + + */ + private array $values = []; + + /** + * The service under test. + * + * @var CustomOverridesService + */ + private CustomOverridesService $service; + + protected function setUp(): void { + parent::setUp(); + + $this->appDir = sys_get_temp_dir() . '/thematiq-perset-' . bin2hex(random_bytes(4)); + mkdir($this->appDir . '/css', 0777, true); + file_put_contents( + $this->appDir . '/token-sets.json', + json_encode( + [ + ['id' => 'nextcloud', 'name' => 'Nextcloud (Base)', 'design_system' => 'none'], + ['id' => 'amsterdam', 'name' => 'Amsterdam', 'design_system' => 'nldesign'], + ] + ) + ); + + $appManager = $this->createMock(IAppManager::class); + $appManager->method('getAppPath')->willReturn($this->appDir); + + $config = $this->createMock(IConfig::class); + $config->method('getAppValue')->willReturnCallback( + fn (string $app, string $key, $default = '') => ($this->values[$key] ?? $default) + ); + + $parser = new CssParserService(); + $darkPalette = new DarkPaletteService(new ContrastService(), $parser, $appManager, $this->createMock(LoggerInterface::class)); + + $this->service = new CustomOverridesService( + $appManager, + $parser, + $darkPalette, + $config, + new DesignSystemService($appManager, $config) + ); + }//end setUp() + + protected function tearDown(): void { + foreach (glob($this->appDir . '/css/*') ?: [] as $file) { + unlink($file); + } + + rmdir($this->appDir . '/css'); + unlink($this->appDir . '/token-sets.json'); + rmdir($this->appDir); + parent::tearDown(); + }//end tearDown() + + /** + * The file name each kind of set is given. + */ + public function testFileForNamesAFileOfItsOwnOnlyForASetOnNoDesignSystem(): void { + $this->assertSame('custom-overrides', CustomOverridesService::fileFor(tokenSet: 'amsterdam', designSystemId: 'nldesign')); + $this->assertSame('custom-overrides-nextcloud', CustomOverridesService::fileFor(tokenSet: 'nextcloud', designSystemId: 'nldesign')); + $this->assertSame('custom-overrides-custom-openwoo', CustomOverridesService::fileFor(tokenSet: 'custom-openwoo', designSystemId: 'none')); + $this->assertSame('custom-overrides-custom-openwoo', CustomOverridesService::fileFor(tokenSet: 'Custom-Open/Woo', designSystemId: 'none')); + }//end testFileForNamesAFileOfItsOwnOnlyForASetOnNoDesignSystem() + + /** + * The stock set writes its own file, and the shared file is untouched. + */ + public function testTheStockSetWritesItsOwnFile(): void { + $this->service->write(tokens: ['--color-primary' => '#112233'], tokenSet: 'nextcloud'); + + $this->assertFileExists($this->appDir . '/css/custom-overrides-nextcloud.css'); + $this->assertFileDoesNotExist($this->appDir . '/css/custom-overrides.css'); + $this->assertSame(['--color-primary' => '#112233'], $this->service->read(tokenSet: 'nextcloud')); + }//end testTheStockSetWritesItsOwnFile() + + /** + * A set on a design system shares the one file, and the stock set does + * not see what it pinned. + */ + public function testASetOnADesignSystemSharesTheFileAndStockDoesNotSeeIt(): void { + $this->service->write(tokens: ['--color-primary' => '#445566'], tokenSet: 'amsterdam'); + + $this->assertFileExists($this->appDir . '/css/custom-overrides.css'); + $this->assertSame([], $this->service->read(tokenSet: 'nextcloud')); + }//end testASetOnADesignSystemSharesTheFileAndStockDoesNotSeeIt() + + /** + * Without a set named, the instance's active set is meant. + */ + public function testNoSetNamedMeansTheActiveSet(): void { + $this->values['token_set'] = 'nextcloud'; + $this->service->ensureExists(); + + $this->assertFileExists($this->appDir . '/css/custom-overrides-nextcloud.css'); + $this->assertSame([], $this->service->read()); + }//end testNoSetNamedMeansTheActiveSet() +}//end class diff --git a/tests/Unit/Service/ThemingServiceTest.php b/tests/Unit/Service/ThemingServiceTest.php index e696c9c4..2b68b596 100644 --- a/tests/Unit/Service/ThemingServiceTest.php +++ b/tests/Unit/Service/ThemingServiceTest.php @@ -368,6 +368,56 @@ function (string $setting, string $value) use (&$written): void { $this->assertArrayNotHasKey('backgroundMime', $written); }//end testACapturedDefaultBackgroundIsRestoredNotRemoved() + /** + * A theme that captured "Remove background image" gets it back, whether + * or not a background colour comes with it. + */ + public function testACapturedRemovedBackgroundIsRemovedAgain(): void { + $written = []; + $this->themingDefaults->method('set')->willReturnCallback( + function (string $setting, string $value) use (&$written): void { + $written[$setting] = $value; + } + ); + + $updated = $this->service->applyColors(params: ['background_mode' => 'color']); + + $this->assertSame(['background_mode'], $updated); + $this->assertSame('backgroundColor', ($written['backgroundMime'] ?? null)); + }//end testACapturedRemovedBackgroundIsRemovedAgain() + + /** + * An unknown background state is refused before anything is written. + */ + public function testAnUnknownBackgroundModeIsRefused(): void { + $this->assertSame( + 'Invalid background_mode: tiled', + $this->service->validateImagePaths(params: ['background_mode' => 'tiled']) + ); + $this->assertNull($this->service->validateImagePaths(params: ['background_mode' => 'default'])); + }//end testAnUnknownBackgroundModeIsRefused() + + /** + * The navigation-bar logo and the favicon are synced like the logo: the + * stored file paired with its mime. + */ + public function testApplyImagesSyncsTheNavigationBarLogoAndFavicon(): void { + $this->imageManager->method('updateImage')->willReturn('image/svg+xml'); + $mimes = []; + $this->themingDefaults->method('set')->willReturnCallback( + function (string $setting, string $value) use (&$mimes): void { + $mimes[$setting] = $value; + } + ); + + $updated = $this->service->applyImages( + params: ['logoheader' => 'img/logos/present.svg', 'favicon' => 'img/logos/present.svg'] + ); + + $this->assertSame(['logoheader', 'favicon'], $updated); + $this->assertSame(['logoheaderMime' => 'image/svg+xml', 'faviconMime' => 'image/svg+xml'], $mimes); + }//end testApplyImagesSyncsTheNavigationBarLogoAndFavicon() + /** * A theme that captured an image of its own never has it removed by the * colour: the image is written by applyImages() right after. diff --git a/tests/Unit/Service/TokenSetServiceCapturedThemingTest.php b/tests/Unit/Service/TokenSetServiceCapturedThemingTest.php new file mode 100644 index 00000000..15ccaeee --- /dev/null +++ b/tests/Unit/Service/TokenSetServiceCapturedThemingTest.php @@ -0,0 +1,132 @@ + + */ + private array $values = []; + + /** + * The service under test. + * + * @var TokenSetService + */ + private TokenSetService $service; + + protected function setUp(): void { + parent::setUp(); + + $this->appDir = sys_get_temp_dir() . '/thematiq-captured-' . uniqid(); + mkdir($this->appDir . '/css/tokens', 0777, true); + file_put_contents($this->appDir . '/css/tokens/amsterdam.css', ':root { --nldesign-color-primary: #ec0000; }'); + file_put_contents( + $this->appDir . '/token-sets.json', + json_encode([['id' => 'amsterdam', 'name' => 'Amsterdam', 'theming' => ['primary_color' => '#ec0000']]]) + ); + + $appManager = $this->createMock(IAppManager::class); + $appManager->method('getAppPath')->willReturn($this->appDir); + + $config = $this->createMock(IConfig::class); + $config->method('getAppValue')->willReturnCallback( + fn (string $app, string $key, $default = '') => ($this->values[$key] ?? $default) + ); + + $cacheFactory = $this->createMock(ICacheFactory::class); + $cacheFactory->method('createDistributed')->willReturn($this->createMock(ICache::class)); + + $this->service = new TokenSetService( + $appManager, + $config, + $this->createMock(LoggerInterface::class), + new ShippedTokenSetAuditService(new ContrastService(), new CssParserService()), + $cacheFactory, + new TokenSetVocabularyAuditService(new CssParserService()) + ); + }//end setUp() + + protected function tearDown(): void { + @unlink($this->appDir . '/css/tokens/amsterdam.css'); + @unlink($this->appDir . '/token-sets.json'); + @rmdir($this->appDir . '/css/tokens'); + @rmdir($this->appDir . '/css'); + @rmdir($this->appDir); + parent::tearDown(); + }//end tearDown() + + /** + * The theming of a set, by id. + * + * @param string $id The set id. + * + * @return array|null The theming block. + */ + private function themingOf(string $id): ?array { + $byId = array_column($this->service->getAvailableTokenSets(), null, 'id'); + return ($byId[$id]['theming'] ?? null); + }//end themingOf() + + /** + * A set's own theming is served while it captured nothing. + */ + public function testAShippedSetKeepsItsOwnThemingUntilItCapturesAny(): void { + $this->assertSame(['primary_color' => '#ec0000'], $this->themingOf('amsterdam')); + }//end testAShippedSetKeepsItsOwnThemingUntilItCapturesAny() + + /** + * Captured branding replaces the set's own theming. + */ + public function testCapturedBrandingReplacesTheSetsOwn(): void { + $captured = ['captured' => true, 'primary_color' => '#23845c', 'background_mode' => 'color']; + $this->values['captured_theming'] = json_encode(['amsterdam' => $captured]); + + $this->assertSame($captured, $this->themingOf('amsterdam')); + }//end testCapturedBrandingReplacesTheSetsOwn() + + /** + * An unreadable captured store is ignored rather than breaking discovery. + */ + public function testAnUnreadableStoreIsIgnored(): void { + $this->values['captured_theming'] = 'not json'; + + $this->assertSame(['primary_color' => '#ec0000'], $this->themingOf('amsterdam')); + }//end testAnUnreadableStoreIsIgnored() +}//end class From 68e432918c75bc533f12f4dfa0036eb0d2540fd7 Mon Sep 17 00:00:00 2001 From: Remko Date: Tue, 29 Sep 2026 09:47:53 +0200 Subject: [PATCH 68/72] chore(phpstan): regenerate the baseline for the branding-capture calls --- phpstan-baseline.neon | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7e370fda..9a99dfae 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -9,9 +9,33 @@ parameters: - message: '#^Call to method hasImage\(\) on an unknown class OCA\\Theming\\ImageManager\.$#' identifier: class.notFound - count: 2 + count: 4 path: lib/Controller/SettingsController.php + - + message: '#^Call to method getImage\(\) on an unknown class OCA\\Theming\\ImageManager\.$#' + identifier: class.notFound + count: 1 + path: lib/Service/BrandingCaptureService.php + + - + message: '#^Call to method hasImage\(\) on an unknown class OCA\\Theming\\ImageManager\.$#' + identifier: class.notFound + count: 1 + path: lib/Service/BrandingCaptureService.php + + - + message: '#^Parameter \$imageManager of method OCA\\Thematiq\\Service\\BrandingCaptureService\:\:__construct\(\) has invalid type OCA\\Theming\\ImageManager\.$#' + identifier: class.notFound + count: 2 + path: lib/Service/BrandingCaptureService.php + + - + message: '#^Property OCA\\Thematiq\\Service\\BrandingCaptureService\:\:\$imageManager has unknown class OCA\\Theming\\ImageManager as its type\.$#' + identifier: class.notFound + count: 2 + path: lib/Service/BrandingCaptureService.php + - message: '#^Access to constant DEFAULT_BACKGROUND_COLOR on an unknown class OCA\\Theming\\Service\\BackgroundService\.$#' identifier: class.notFound @@ -27,13 +51,13 @@ parameters: - message: '#^Call to method set\(\) on an unknown class OCA\\Theming\\ThemingDefaults\.$#' identifier: class.notFound - count: 3 + count: 4 path: lib/Service/ThemingService.php - message: '#^Call to method undo\(\) on an unknown class OCA\\Theming\\ThemingDefaults\.$#' identifier: class.notFound - count: 1 + count: 2 path: lib/Service/ThemingService.php - From 57a25e725d144a41a43126ccd6a54f0e3030dcf6 Mon Sep 17 00:00:00 2001 From: Remko Date: Tue, 29 Sep 2026 09:50:52 +0200 Subject: [PATCH 69/72] refactor(themes): split upload, applyColors and getAvailableTokenSets below the phpmd thresholds --- lib/Controller/CustomTokenSetController.php | 86 ++++++++++++++------- lib/Service/ThemingService.php | 14 ++++ lib/Service/TokenSetService.php | 27 +++++-- 3 files changed, 94 insertions(+), 33 deletions(-) diff --git a/lib/Controller/CustomTokenSetController.php b/lib/Controller/CustomTokenSetController.php index ab3b9b27..14b80b52 100644 --- a/lib/Controller/CustomTokenSetController.php +++ b/lib/Controller/CustomTokenSetController.php @@ -254,39 +254,73 @@ public function upload(): JSONResponse { // Uploads are untouched — a file an admin picks off disk is still an // unknown document and still goes through the converter. if ($this->request->getParam('raw', false) === true) { - $parsed = $this->mapFromCss(content: $read['content'], slug: $slug); - if ($parsed instanceof JSONResponse) { - return $parsed; - } + return $this->storeRaw(name: $name, slug: $slug, content: $read['content']); + } - $parsed['css'] = $read['content']; + return $this->storeConverted(name: $name, slug: $slug, read: $read); + }//end upload() - // Which design system the editor was looking at when it serialised - // this. Allow-listed against the shipped manifest rather than taken - // on trust: it decides which stylesheet layers every page load emits. - $claimed = trim((string)$this->request->getParam('designSystem', '')); - if ($claimed !== '' && isset($this->designSystems->getDesignSystems()[$claimed]) === true) { - $parsed['designSystem'] = $claimed; - } + /** + * Store a token set that arrived already in the `css/tokens/*.css` shape. + * + * @param string $name The set's display name. + * @param string $slug The slug derived from the name. + * @param string $content The token set CSS, as sent. + * + * @return JSONResponse The persisted set, or the validator's error. + * + * @spec openspec/changes/nlds-theme-converter/specs/custom-token-sets/spec.md + */ + private function storeRaw(string $name, string $slug, string $content): JSONResponse { + $parsed = $this->mapFromCss(content: $content, slug: $slug); + if ($parsed instanceof JSONResponse) { + return $parsed; + } + + $parsed['css'] = $content; - $response = $this->persist(name: $name, parsed: $parsed); - - // A theme saved from the editor keeps the Nextcloud branding that - // was on when it was saved — colours, background, logos, favicon — - // so applying it later brings that back too. Only once the set is - // safely stored: a refused save must not leave copied images behind. - if ($this->request->getParam('captureTheming', false) === true - && $this->brandingCapture !== null - && $response->getStatus() === 200 - ) { - $data = $response->getData(); + // Which design system the editor was looking at when it serialised + // this. Allow-listed against the shipped manifest rather than taken + // on trust: it decides which stylesheet layers every page load emits. + $claimed = trim((string)$this->request->getParam('designSystem', '')); + if ($claimed !== '' && isset($this->designSystems->getDesignSystems()[$claimed]) === true) { + $parsed['designSystem'] = $claimed; + } + + $response = $this->persist(name: $name, parsed: $parsed); + + // A theme saved from the editor keeps the Nextcloud branding that + // was on when it was saved — colours, background, logos, favicon — + // so applying it later brings that back too. Only once the set is + // safely stored: a refused save must not leave copied images behind. + if ($this->request->getParam('captureTheming', false) === true + && $this->brandingCapture !== null + && $response->getStatus() === 200 + ) { + // persist() always answers with an array; getData() is typed + // array|object, so say which before indexing it. + $data = $response->getData(); + if (is_array($data) === true) { $data['theming'] = $this->brandingCapture->capture(setId: (string)$data['id']); $response->setData($data); } - - return $response; } + return $response; + }//end storeRaw() + + /** + * Convert an uploaded design-system document and store the result. + * + * @param string $name The set's display name. + * @param string $slug The slug derived from the name. + * @param array{content: string, sourceName: string|null} $read The payload from readInput(). + * + * @return JSONResponse `{ id, imported, skipped, warnings, report, counts, inputKind }` or an error. + * + * @spec openspec/changes/nlds-theme-converter/specs/custom-token-sets/spec.md + */ + private function storeConverted(string $name, string $slug, array $read): JSONResponse { try { $converted = $this->converter->convert( content: $read['content'], @@ -367,7 +401,7 @@ public function upload(): JSONResponse { } return $this->persist(name: $name, parsed: $parsed); - }//end upload() + }//end storeConverted() /** * Read the import payload from either the file picker or the paste box. diff --git a/lib/Service/ThemingService.php b/lib/Service/ThemingService.php index 23cbd2dc..65587dd6 100644 --- a/lib/Service/ThemingService.php +++ b/lib/Service/ThemingService.php @@ -201,6 +201,20 @@ public function applyColors(array $params): array { } } + return $this->applyBackgroundMode(params: $params, updated: $updated); + }//end applyColors() + + /** + * Put the background in the state the theme asks for, after its colours. + * + * @param array $params The request parameters. + * @param array $updated The colour keys applyColors() already wrote. + * + * @return array The updated keys, plus `background_mode` when a mode was applied. + * + * @spec openspec/changes/retrofit-2026-05-24-annotate-nldesign/tasks.md#task-43 + */ + private function applyBackgroundMode(array $params, array $updated): array { // A theme that CAPTURED Nextcloud's branding says which of the three // background states it was saved in, and gets exactly that back: // "Remove background image" (`color`), Nextcloud's own default image diff --git a/lib/Service/TokenSetService.php b/lib/Service/TokenSetService.php index c593428f..1ca91a91 100644 --- a/lib/Service/TokenSetService.php +++ b/lib/Service/TokenSetService.php @@ -238,13 +238,7 @@ public function getAvailableTokenSets(): array { // BrandingCaptureService). It replaces the set's own theming block: // it is what the admin had on when they saved this theme, which is // what applying the theme is meant to bring back. - $captured = json_decode( - (string)$this->config->getAppValue(Application::APP_ID, BrandingCaptureService::CAPTURED_KEY, '{}'), - true - ); - if (is_array($captured) === false) { - $captured = []; - } + $captured = $this->readCapturedTheming(); // Scan filesystem for actual CSS files. $tokenSets = []; @@ -294,6 +288,25 @@ public function getAvailableTokenSets(): array { return $tokenSets; }//end getAvailableTokenSets() + /** + * The Nextcloud branding each set captured when it was saved, by set id. + * + * @return array The captured theming blocks; empty when none are stored or the value is unreadable. + * + * @spec openspec/specs/token-sets/spec.md + */ + private function readCapturedTheming(): array { + $captured = json_decode( + (string)$this->config->getAppValue(Application::APP_ID, BrandingCaptureService::CAPTURED_KEY, '{}'), + true + ); + if (is_array($captured) === false) { + return []; + } + + return $captured; + }//end readCapturedTheming() + /** * Get the token sets an admin may select: `SELECTABLE_SHIPPED_SETS` plus * every admin-imported `custom-*` set. From 3102dc60931e78366230e5611aec8e09d07c93a9 Mon Sep 17 00:00:00 2001 From: Remko Date: Tue, 29 Sep 2026 09:53:22 +0200 Subject: [PATCH 70/72] docs(themes): link the branding capture methods to the theming-sync spec capture(), all() and forget() carry the @spec reference to the theming metadata requirement they implement, as the rest of the service layer does. --- lib/Service/BrandingCaptureService.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/Service/BrandingCaptureService.php b/lib/Service/BrandingCaptureService.php index 7d6ac8b1..4916ee27 100644 --- a/lib/Service/BrandingCaptureService.php +++ b/lib/Service/BrandingCaptureService.php @@ -123,6 +123,8 @@ public function __construct( * ARE those files right now — without it the next apply of this very theme * would offer to "change" every image to itself. * + * @spec openspec/specs/theming-sync/spec.md#requirement-theming-metadata-in-token-sets + * * @param string $setId The token set the branding belongs to. * * @return array The captured theming block. @@ -171,6 +173,8 @@ public function capture(string $setId): array { /** * Every set's captured branding, by set id. * + * @spec openspec/specs/theming-sync/spec.md#requirement-theming-metadata-in-token-sets + * * @return array> The captured blocks. */ public function all(): array { @@ -185,6 +189,8 @@ public function all(): array { /** * Forget a set's captured branding and remove its copied images. * + * @spec openspec/specs/theming-sync/spec.md#requirement-theming-metadata-in-token-sets + * * @param string $setId The token set. * * @return void From 20ca4d914174569b6bb7540028e62e980fa2a449 Mon Sep 17 00:00:00 2001 From: Remko Date: Tue, 29 Sep 2026 09:53:23 +0200 Subject: [PATCH 71/72] fix(a11y): mark the preview table's header cells as column headers The app mock's table now has text headers, so each th says which cells it heads with scope="col". --- templates/settings/admin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/settings/admin.php b/templates/settings/admin.php index f3921ec8..3dc38855 100644 --- a/templates/settings/admin.php +++ b/templates/settings/admin.php @@ -252,7 +252,7 @@ class="checkbox" - + From 74a61d7012d3329795db29103314e39312b955eb Mon Sep 17 00:00:00 2001 From: Remko Date: Tue, 29 Sep 2026 10:01:57 +0200 Subject: [PATCH 72/72] fix(gates): add @spec tags to the branding capture service, scope the mini table headers and capitalise an inline comment --- lib/Controller/CustomTokenSetController.php | 2 +- lib/Service/BrandingCaptureService.php | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/Controller/CustomTokenSetController.php b/lib/Controller/CustomTokenSetController.php index 14b80b52..0237d71a 100644 --- a/lib/Controller/CustomTokenSetController.php +++ b/lib/Controller/CustomTokenSetController.php @@ -297,7 +297,7 @@ private function storeRaw(string $name, string $slug, string $content): JSONResp && $this->brandingCapture !== null && $response->getStatus() === 200 ) { - // persist() always answers with an array; getData() is typed + // The persist() response is always an array; getData() is typed // array|object, so say which before indexing it. $data = $response->getData(); if (is_array($data) === true) { diff --git a/lib/Service/BrandingCaptureService.php b/lib/Service/BrandingCaptureService.php index 4916ee27..30246d97 100644 --- a/lib/Service/BrandingCaptureService.php +++ b/lib/Service/BrandingCaptureService.php @@ -45,6 +45,8 @@ * custom alike — because a shipped set's metadata is a read-only file in the * app, and one store for both keeps "what this theme remembers" in one place. * {@see TokenSetService} lays it over the set's own theming. + * + * @spec openspec/specs/theming-sync/spec.md#requirement-theming-metadata-in-token-sets */ class BrandingCaptureService { @@ -123,11 +125,11 @@ public function __construct( * ARE those files right now — without it the next apply of this very theme * would offer to "change" every image to itself. * - * @spec openspec/specs/theming-sync/spec.md#requirement-theming-metadata-in-token-sets - * * @param string $setId The token set the branding belongs to. * * @return array The captured theming block. + * + * @spec openspec/specs/theming-sync/spec.md#requirement-theming-metadata-in-token-sets */ public function capture(string $setId): array { $this->deleteFiles(setId: $setId); @@ -173,9 +175,9 @@ public function capture(string $setId): array { /** * Every set's captured branding, by set id. * - * @spec openspec/specs/theming-sync/spec.md#requirement-theming-metadata-in-token-sets - * * @return array> The captured blocks. + * + * @spec openspec/specs/theming-sync/spec.md#requirement-theming-metadata-in-token-sets */ public function all(): array { $decoded = json_decode((string)$this->config->getAppValue(Application::APP_ID, self::CAPTURED_KEY, '{}'), true); @@ -189,11 +191,11 @@ public function all(): array { /** * Forget a set's captured branding and remove its copied images. * - * @spec openspec/specs/theming-sync/spec.md#requirement-theming-metadata-in-token-sets - * * @param string $setId The token set. * * @return void + * + * @spec openspec/specs/theming-sync/spec.md#requirement-theming-metadata-in-token-sets */ public function forget(string $setId): void { $this->deleteFiles(setId: $setId);
t('Name')); ?>t('Date')); ?>t('Status')); ?>
t('Name')); ?>t('Date')); ?>t('Status')); ?>