Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load a theme via a theme key in volto.config.js or in package.json #4625

Merged
merged 19 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions addon-registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,11 @@ class AddonConfigurationRegistry {
this.packages = {};
this.customizations = new Map();

// Theme from a package.json key, from volto.config.js or from an ENV VAR
// Programatically via volto.config.js wins or the ENV VAR if present
this.theme =
packageJson.theme || this.voltoConfigJS.theme || process.env.THEME;

this.initDevelopmentPackages();
this.initPublishedPackages();
this.initAddonsFromEnvVar();
Expand Down Expand Up @@ -354,6 +359,35 @@ class AddonConfigurationRegistry {
.filter((e) => e);
}

getCustomThemeAddons() {
const customThemeAddonsInfo = {
variables: [],
main: [],
};

this.getAddonDependencies().forEach((addon) => {
const normalizedAddonName = addon.split(':')[0];
// We have two possible insertion points, variables and main

const customThemeVariables = `${this.packages[normalizedAddonName].modulePath}/theme/_variables.scss`;
const customThemeMain = `${this.packages[normalizedAddonName].modulePath}/theme/_main.scss`;
if (
fs.existsSync(customThemeVariables) &&
normalizedAddonName !== this.theme
) {
customThemeAddonsInfo.variables.push(normalizedAddonName);
}
if (
fs.existsSync(customThemeMain) &&
normalizedAddonName !== this.theme
) {
customThemeAddonsInfo.main.push(normalizedAddonName);
}
});

return customThemeAddonsInfo;
}

/**
* Returns a mapping name:diskpath to be uses in webpack's resolve aliases
*/
Expand Down
79 changes: 79 additions & 0 deletions create-theme-addons-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const path = require('path');
const fs = require('fs');
const tmp = require('tmp');
const cryptoRandomString = require('crypto-random-string');

const titleCase = (w) => w.slice(0, 1).toUpperCase() + w.slice(1, w.length);

/*
* Transforms a package name to javascript variable name
*/
function nameFromPackage(name) {
name =
name.replace(/[@~./\\:\s]/gi, '') ||
cryptoRandomString({ length: 10, characters: 'abcdefghijk' });
return name
.split('-')
.map((w, i) => (i > 0 ? titleCase(w) : w))
.join('');
}

/*
* Creates a static file with code necessary to load the addons configuration
*
*/
function getAddonsLoaderCode(name, customThemeAddons = []) {
let buf = `/*
This file is autogenerated. Don't change it directly.
Add a ./theme/_${name}.scss in your add-on to load your theme customizations in the current theme.
*/

`;
customThemeAddons.forEach((addon) => {
const customization = `${addon}/theme/${name}`;
const line = `@import '${customization}';\n`;
buf += line;
});

return buf;
}

module.exports = ({ main, variables }) => {
// const addonsThemeLoaderVariablesPath = path.join(
// process.cwd(),
// 'src',
// '_variables.scss',
// );
// const addonsThemeLoaderMainPath = path.join(
// process.cwd(),
// 'src',
// '_main.scss',
// );

// const addonsThemeLoaderVariablesPath = path.join(
// process.cwd(),
// 'src',
// '_variables.scss',
// );
// const addonsThemeLoaderMainPath = path.join(
// process.cwd(),
// 'src',
// '_main.scss',
// );

const addonsThemeLoaderVariablesPath = tmp.tmpNameSync({ postfix: '.scss' });
const addonsThemeLoaderMainPath = tmp.tmpNameSync({ postfix: '.scss' });
fs.writeFileSync(
addonsThemeLoaderVariablesPath,
new Buffer.from(getAddonsLoaderCode('variables', variables)),
);
fs.writeFileSync(
addonsThemeLoaderMainPath,
new Buffer.from(getAddonsLoaderCode('main', main)),
);

return [addonsThemeLoaderVariablesPath, addonsThemeLoaderMainPath];
};

module.exports.getAddonsLoaderCode = getAddonsLoaderCode;
module.exports.nameFromPackage = nameFromPackage;
1 change: 1 addition & 0 deletions docs/source/addons/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ myst:

i18n
best-practices
theme
```

There are several advanced scenarios where we might want to have more control
Expand Down
220 changes: 220 additions & 0 deletions docs/source/addons/theme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
---
myst:
html_meta:
"description": "Create a theme add-on"
"property=og:description": "Create a theme add-on"
"property=og:title": "Create a theme add-on"
"keywords": "Volto, Plone, Semantic UI, CSS, Volto theme"
---

# Create a theme add-on

We can create a Volto Add-on that acts as a theme Add-on, so we can detach it from the project.
The advantage is that you can deploy the same theme in different projects, or have themes depending on conditions that you could inject on build time.
For convenience, it can also be set via a `THEME` environment variable.

1. Add a `theme` key in your `volto.config.js` file in the root of your project:
ksuess marked this conversation as resolved.
Show resolved Hide resolved

```js
module.exports = {
addons: [],
theme: 'volto-my-theme'
};
```

or add a key in your `package.json` project:

```json
"theme": "volto-my-theme"
```

or via a `THEME` variable:

```shell
THEME='volto-my-theme' yarn start
```

2. Create a directory `src/theme` in your add-on, then add this file `theme.config`, replacing `<name_of_your_theme>` with your add-on name:

```less
/*******************************
Theme Selection
*******************************/

/* To override a theme for an individual element specify theme name below */

/* Global */
@site : 'pastanaga';
@reset : 'pastanaga';

/* Elements */
@button : 'pastanaga';
@container : 'pastanaga';
@divider : 'pastanaga';
@flag : 'pastanaga';
@header : 'pastanaga';
@icon : 'pastanaga';
@image : 'pastanaga';
@input : 'pastanaga';
@label : 'pastanaga';
@list : 'pastanaga';
@loader : 'pastanaga';
@placeholder : 'pastanaga';
@rail : 'pastanaga';
@reveal : 'pastanaga';
@segment : 'pastanaga';
@step : 'pastanaga';

/* Collections */
@breadcrumb : 'pastanaga';
@form : 'pastanaga';
@grid : 'pastanaga';
@menu : 'pastanaga';
@message : 'pastanaga';
@table : 'pastanaga';

/* Modules */
@accordion : 'pastanaga';
@checkbox : 'pastanaga';
@dimmer : 'pastanaga';
@dropdown : 'pastanaga';
@embed : 'pastanaga';
@modal : 'pastanaga';
@nag : 'pastanaga';
@popup : 'pastanaga';
@progress : 'pastanaga';
@rating : 'pastanaga';
@search : 'pastanaga';
@shape : 'pastanaga';
@sidebar : 'pastanaga';
@sticky : 'pastanaga';
@tab : 'pastanaga';
@transition : 'pastanaga';

/* Views */
@ad : 'pastanaga';
@card : 'pastanaga';
@comment : 'pastanaga';
@feed : 'pastanaga';
@item : 'pastanaga';
@statistic : 'pastanaga';

/* Extras */
@main : 'pastanaga';
@custom : 'pastanaga';

/*******************************
Folders
*******************************/

/* Path to theme packages */
@themesFolder : '~volto-themes';

/* Path to site override folder */
@siteFolder : "~<name_of_your_theme>/theme";
sneridagh marked this conversation as resolved.
Show resolved Hide resolved

/*******************************
Import Theme
*******************************/

@import (multiple) "~semantic-ui-less/theme.less";
@fontPath : "~volto-themes/@{theme}/assets/fonts";

.loadAddonOverrides() {
@import (optional) "@{siteFolder}/@{addon}/@{addontype}s/@{addonelement}.overrides";
}

/* End Config */
```

3. Declare the theme as an add-on by adding its name as the value for the `addons` key in either `volto.config.js` or `package.json`.
sneridagh marked this conversation as resolved.
Show resolved Hide resolved
4. After starting Volto, the theme should be active.
Now you can add overrides to the default theme in `src/theme`, same as you would in a project.
5. Now you can safely delete your project's `theme` folder, since the one in the add-on will take precedence.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should I delete the theme folder of the project. I want to use the theme, but use a different font.
Can I customize with src/theme/extras/custom.overrides for example heading sizes and fonts?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that we could state better the history behind the add-on themes and why after this, you don't need a project theme anymore (since only one is active at the same time). Effectively you are moving the theme from the project to an add-on, with all its consequences, so now your theme lives there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "only one (theme) is active at the same time" needs explanation.
The naive understanding of a theme is just the collection of some CSS rules, maybe variables. But here you mean what exactly?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think "only one (theme) is active at the same time".
The naive understanding of a theme is just a collection of CSS rules and maybe variables. How would you explain that thing that is the only one activated?


## Using your own escape hatch
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe start with a first line about why and when a hatch is needed. From an integrater point of view, without having built multiple themes before.


Volto theming uses SemanticUI theming capabilities to define and extend a theme for your site.
However, while maintaining and playing well with the Semantic UI Volto base, using a traditional CSS approach can be done using the Less-based `extras` escape hatch.

At the same time, one can entirely discard the extras escape hatch and add your own, by customizing the `theme.js` module in Volto.

```js
import 'semantic-ui-less/semantic.less';
import '@plone/volto/../theme/themes/pastanaga/extras/extras.less';

// You can add more entry points for theming
import '@kitconcept/volto-light-theme/theme/main.scss';
```

Customizing it is a special use case in Volto: add a `./@root/theme.js` file structure in your `customizations` folder in your add-on or project.
sneridagh marked this conversation as resolved.
Show resolved Hide resolved

While building your own escape hatch for theming, you can use the preprocessor of your choice (in the example, SCSS) while maintaining the "base" Volto theme, but customizing it using the resultant CSS.

You can see an example of such a theme in: https://github.com/kitconcept/volto-light-theme

## Modify a custom theme from another add-on
ksuess marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

@ksuess ksuess Apr 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still not clear to me, what part of this section are instructions for the reusable theme, and which instructions belong to the add-on which slightly customizes the theme.
I stop bothering now, as I planned to take some days of from yesterday on.
Thanks for the clarifications so far. I'll come back with questions later, like: "How would I use a reusable theme like kitconcept/volto-light-theme, but with another text color (probably $text-color ? But where? /theme/custom/_main.scss ?)"

A first try, wich is not OK so far: https://github.com/rohberg/project-with-theme

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah no worries, thanks for the valuable feedback! We will continue developing this and the story behind, and improving docs. The plan to work on this and the future of theming in Volto during Beethoven Sprint.


Sometimes you have a custom theme that you want to reuse through all your projects, but with some differences, maintaining the base.
Usually, the only option would be to use an add-on that adds more CSS to the base theme, using imports that will load after the theme.
However, there is a problem with this approach.
You cannot use existing theme variables, including breakpoints, on these new styles.
Similarly, it gets somewhat detached from the normal flow of the loaded theme.
The same applies for add-ons, as they are detached from the current theme.
One could use a SemanticUI approach for making this work, but it's SemanticUI bound.

```{warning}
This is only possible when using your own escape hatch, and works only with SCSS-based themes, and not with SemanticUI themes, since it enables a couple of entry points that only support SCSS files.
For an example of how it could be used, see: https://github.com/kitconcept/volto-light-theme
```

If your custom escape hatch defines a custom theme using SCSS, you can take advantage of this feature.
Although not limited to this, it would be possible to extend this feature to add more entry points, using another preprocessor or theming approach.

This feature enables two entry points: variables and main.
From your add-on code, you can extend an existing theme by creating a file corresponding to each entry point:

* `./src/theme/_variables.scss`
* `./src/theme/_main.scss`

### Variables (`addonsThemeCustomizationsVariables`)

Use this entry point file to modify the original variables of the current loaded theme by adding the entry point before the theme variable definitions.
In the theme, it should be imported as shown below:

```scss hl_lines="2"
@import 'addonsThemeCustomizationsVariables';
@import 'variables';
@import 'typography';
@import 'utils';
@import 'layout';
```

```{warning}
Following SCSS best practices, your theme variables should be "overridable" using the `!default` keyword. Then your customizations should be loaded before them. For more information: https://sass-lang.com/documentation/variables#default-values
sneridagh marked this conversation as resolved.
Show resolved Hide resolved
```

Volto will not only load your add-on entry point files, but it will also detect all the add-ons that have these entry point files and import them grouped under a single file.
It will also automatically add an `addonsThemeCustomizationsVariables` alias that can be referenced from the theme as shown above.

### Main (`addonsThemeCustomizationsMain`)

This entry point is intended to add your own style definitions, complementing those in the theme.
You should add it after all the CSS of your theme:

```scss hl_lines="6"
@import 'blocks/search';
@import 'blocks/listing';

@import 'temp';

@import 'addonsThemeCustomizationsMain';

/* No CSS beyond this point */
```

Volto will also detect all the add-ons that have these entry point files, and import them grouped under a single file, and will automatically add an `addonsThemeCustomizationsMain` alias that can be referenced from the theme as shown above.

```{note}
It will only work in combination with the theme declaration in `volto.config.js` or in `package.json`.
```
2 changes: 2 additions & 0 deletions news/4625.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Support for declaring a theme in `volto.config.js` or in `package.json`
Add two entry points to allow extension of a theme from other add-ons. @sneridagh
23 changes: 23 additions & 0 deletions razzle.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const fs = require('fs');
const RootResolverPlugin = require('./webpack-plugins/webpack-root-resolver');
const RelativeResolverPlugin = require('./webpack-plugins/webpack-relative-resolver');
const createAddonsLoader = require('./create-addons-loader');
const createThemeAddonsLoader = require('./create-theme-addons-loader');
const AddonConfigurationRegistry = require('./addon-registry');
const CircularDependencyPlugin = require('circular-dependency-plugin');
const TerserPlugin = require('terser-webpack-plugin');
Expand Down Expand Up @@ -245,6 +246,28 @@ const defaultModify = ({
'lodash-es': path.dirname(require.resolve('lodash')),
};

const [
addonsThemeLoaderVariablesPath,
addonsThemeLoaderMainPath,
] = createThemeAddonsLoader(registry.getCustomThemeAddons());

// Automatic Theme Loading
if (registry.theme) {
// The themes should be located in `src/theme`
const themePath = registry.packages[registry.theme].modulePath;
const themeConfigPath = `${themePath}/theme/theme.config`;
config.resolve.alias['../../theme.config$'] = themeConfigPath;
config.resolve.alias['../../theme.config'] = themeConfigPath;

// We create an alias for each custom theme insertion point (variables, main)
config.resolve.alias[
'addonsThemeCustomizationsVariables'
] = addonsThemeLoaderVariablesPath;
config.resolve.alias[
'addonsThemeCustomizationsMain'
] = addonsThemeLoaderMainPath;
}

config.performance = {
maxAssetSize: 10000000,
maxEntrypointSize: 10000000,
Expand Down