Conversation
Pin the minimum in the test extra and record the override in mx.ini, so a checkout resolves the version the suite is written against.
Color validates a hex value and reaches Volto as a colorPicker widget. It also has to survive a round-trip through the registry, which the upcoming named themes are built on. plone.registry resolves the persistent equivalent of a field by looking up its class name in plone.registry.field (see fieldfactory.persistentFieldAdapter). Color has no entry there, and neither does its NativeStringLine base, so registerInterface raises TypeError before writing a single record. Ship a PersistentColor and register an IPersistentField adapter on the narrower IColor so it wins over the generic one registered on IField.
A theme is a set of records under sc.voltolighttheme.theme.<id>, described by ISCVLTThemeDefinition. The layout follows plone.app.querystring's operations: one records block per theme, discovered by walking the registry and filtering on the prefix rather than through an index. utils.themes holds the enumeration and CRUD helpers so the vocabulary, the control panel and the field serializer share one implementation of the prefix split. theme_settings() returns only the style-bearing fields, defined by excluding the metadata ones, so a style variable added to the schema is picked up without touching the module. Theme ids may not contain a dot, since the id is a segment of the record name and a dot would corrupt enumeration. The shipped default theme cannot be deleted, only edited.
Managers list, create, edit, duplicate and delete themes through @controlpanels/themes. No dedicated service is needed: plone.restapi already traverses sub-paths of a panel into add/get/update/delete, so a RegistryConfigletPanel subclass covers every verb. The panel is a collection rather than a single set of records, which the default serializer cannot express -- it reads one prefix through registry.forInterface. ThemesControlpanelSerializeToJson returns the schema once and the themes as items instead, so the frontend can drive the existing form machinery, colorPicker widgets included. Deleting the default theme is refused; editing it is not. A theme still selected by content can be deleted, since themes are not catalogued and the reference cannot be checked cheaply -- the field serializer degrades instead.
Replace the kitconcept.voltolighttheme behaviors with sc.voltolighttheme ones,
split by concern: a site header, an intranet header extending it, and a footer.
former_dotted_names keeps the old header name resolvable.
IIntranetHeaderSettings inherits from ISiteHeaderSettings, so its fieldset must
list only the fields it adds. plone.autoform merges same-named fieldsets across
the inheritance chain by appending, and z3c.form rejects a repeated field name
with ValueError("Duplicate name", ...) -- which surfaces as a 500 on
@types/<type>, breaking the edit form for every type using the behavior.
order_after restores the intended field order that re-listing would have given.
sc.voltolighttheme.themeselector carries a single choice field backed by the themes vocabulary. It is enabled on Plone Site, and may be added to any other type: the @inherit expander resolves the closest provider up the acquisition chain, so a section overrides its parent and content is never left without a theme. The field serializes to {token, title, value}, which is not decorative. Reading, normalizeSingleSelectOption resolves the selected option as value.token ?? value.value ?? value.UID ?? 'no-value', so a payload without token renders as "No value" however complete it is. Writing, plone.restapi's ChoiceFieldDeserializer unwraps value["token"] from a mapping, so an untouched field survives a PATCH of the whole form. The resolved settings ride under value without displacing that contract. A theme deleted while still selected keeps its token and gets an empty value, so a dangling reference degrades to the stylesheet defaults instead of breaking the content response. Allow S105 in tests: "token" is a vocabulary term here, not a secret.
useLiveData maps the kitconcept behavior names onto ours, resolved per call so a header behavior configured after import is honoured, and passing unmapped names through so a third-party behavior still resolves. The values it reads used to come only from content.data, which blanks on every route change out of the CMS UI: protectLoadStart sets resetBeforeFetch whenever the departing route isCmsUi, protectLoadEnd then dispatches RESET_CONTENT ahead of GET_CONTENT_PENDING, and UNLOCK_CONTENT_SUCCESS repopulates content.data without an @components key. For that window the header and footer render empty. Hold the expander in its own slice instead, modelled on Volto's navigation reducer: no GET_CONTENT_PENDING case, and a response carrying no inherit key leaves the previous values in place. A response that does carry it is authoritative and replaces the slice, since a behavior missing from it has no provider up the chain. vitest could not resolve react-redux and friends from this package -- they are Volto's dependencies -- so the aliases point at its copies.
Theming replaces the upstream component of the same name through a customization, so it inherits upstream's own aboveHeader slot registration rather than adding a second one that would emit the rule twice. Field names map onto custom properties mechanically -- primary_color becomes --primary-color -- so a setting added to ISCVLTThemeDefinition needs no change here. header_foreground_color is the exception in reverse: --header-foreground is read by upstream VLT as well as by us, so the token keeps its name and _root.scss resolves it through var(--header-foreground-color, ...), which also makes the override independent of stylesheet order. Values are validated before they are written into a style element, since a value that escapes its declaration injects arbitrary CSS. Fields named *_color must be a hex colour, the same rule the backend Color field enforces; anything else has to be free of the constructs that end a declaration, open a rule, or fetch and evaluate. Unknown settings are allowed through that second check rather than dropped, so a new kind of setting works as soon as it exists. SiteThemeSettings described six colours where the backend sends seven; the types now mirror ISCVLTThemeDefinition, with the old name kept as a deprecated alias.
A listing with colour swatches, plus Volto's own Form for add and edit, so the colorPicker widgets and validation come from the served schema rather than being reimplemented. The four core controlpanel actions already match the API, updateControlpanel included -- it takes a URL, so it patches an individual theme. Volto's generic /controlpanel/:id route renders a single-schema registry form, which cannot express a collection; addon routes are matched first, so the entry takes over /controlpanel/themes. A theme can be duplicated from an existing one: the settings and description are copied, the identity is not, and the name is suffixed so two themes never look alike in the listing. The default theme is editable like any other and only its delete control is hidden. The id is not part of ISCVLTThemeDefinition -- it lives in the record prefix -- so the add schema grows one, and the edit schema deliberately does not: a theme cannot be renamed in place.
themes and defaultTheme are a kitconcept convention that Volto's BlockConfigBase does not declare, so reading them off blocksConfig failed to typecheck. Declare them through a module augmentation, the same approach settings.ts already uses for SettingsConfig. The two block views each redeclared the ThemeDefinition shape; they now import the one config/blocks.ts exports.
…d updated control panel logic
The package ships header, theme and footer behaviors whose fields overlap ours field for field. Leaving both registered means two of everything on the site root's edit form, and the FTI has to pick a winner per fieldset. z3c.unconfigure removes them at ZCML time, which is the only point where a plone:behavior registration can be undone. The overrides are loaded through includeOverrides under a zcml:condition, so the file is inert when the package is not installed -- it has to be, since it includes kitconcept's own configure.zcml to have something to unconfigure. kitconcept.sticky_menu stays: nothing here replaces it.
An intranet site root wants the intranet header rather than the public one, and the two behaviors are mutually exclusive -- both provide a header fieldset. A Plone_Site.xml with purge="true" is the whole difference, so the profile only carries that one file. It is registered as an EXTENSION profile and hidden from the add-ons control panel: it is meant to be applied by a distribution at site creation, not installed by hand afterwards, since swapping the header behavior on a populated site drops the fields the other one stored. The default profile's behavior list gains the theme selector ahead of the header, and loses the two commented-out entries that outlived the behaviors they named.
A festive theme with hardcoded reds and greens is a demonstration of what the control panel can express, not something a plain installation should ship. It lives in the initial profile now, beside the example content it belongs with, and the default profile is left with the one theme that is actually a default.
Everything the theme work added to the default profile -- the theme records, the Plone Site FTI, the control panel entry -- only reaches an existing site through an upgrade step, and the kitconcept profile it used to depend on has to be uninstalled rather than merely dropped from metadata.xml: a dependency that disappears leaves its imported settings in place. The step reimports the three affected GenericSetup steps and runs kitconcept.voltolighttheme:uninstall. Removing the dependency is what makes that safe -- reinstalling our default profile would otherwise pull the package straight back in.
dummy_type_schema and create_dummy_content were defined identically in tests/conftest.py and tests/behaviors/conftest.py. The package-level copies shadowed the root ones and were the only definitions that could run: the root copies depend on role_request, which existed solely in the file being shadowed. role_request and editor_request move up with them. portal_factory grows a container flag. themed_portal used to declare its own Themed FTI -- a third place building a dummy type -- and the only thing it needed that portal_factory could not give was a Container klass, so a section can hold pages. It now builds on portal_factory and registers the corporate theme before the content that selects it, since the field validates against the vocabulary. The theme selector tests take the same shape as the other behavior tests as a result. Two of them were dead before this: the inherit expander suite referenced an undefined BEHAVIOR, and the behavior fixture named siteheader.
The panel was one 347-line file holding the store access, the state machine and every piece of markup. It is now a connected container over a set of presentational components -- ThemesUI, ThemesList, ThemeForm, ThemesToolbarActions, ThemeSwatches -- each with a Storybook story, so the four states the panel can be in can be seen without a running site. The authorization check moves out of the route and into the component. The route wrapper only knew whether a token existed, which is true for any logged in user; an editor without the permission got the panel and then a 401 from every action it offered. The panel now renders Unauthorized when the fetch itself is refused. It reads that failure from controlpanels.get.error rather than from the dispatch promise, since the API middleware returns rather than rethrows in its rejection handler. Two things the stories turned up, both invisible until something actually rendered: Volto's Form and the Fields inside it are loadable components, so a jsdom test can only ever assert the absence of a chunk loader. The unit tests pin the props handed to Form -- which schema, which starting data -- and the stories are what prove it renders. A field named id is mapped by Volto to IdWidget, by field name, and that widget selects state.querystring.indexes. The add form's id field therefore renders Volto's content-URL widget rather than a text input, which is worth revisiting separately. The story fixture declares its colour fields as plain strings: upstream's colour picker reads config.settings.colorMap[props.id] unguarded, and Storybook boots no add-on config, so a colorPicker field throws during render and leaves an empty frame.
The storybook job was already wired into the frontend workflow but never asked for. It also read its Node version from a job output that does not exist in that scope, so the input is used directly.
ericof
marked this pull request as ready for review
August 24, 2026 02:53
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ships our own header, footer and theme-selector behaviors, replacing the ones
inherited from
kitconcept.voltolighttheme, and introduces named themes:managers define them in a new Themes control panel, editors pick one per site or
per section, and the frontend applies them as CSS custom properties.
Closes #20
Backend
A
Colorfield storable in the registryfields/color.py— aColorschema field validating#rgb/#rrggbb.fields/persistence.py—PersistentColorplus anIPersistentFieldadapterregistered on
IColor.plone.registryresolves persistent fields by class name(
getattr(plone.registry.field, type(field).__name__, None)), so a customfield with no persistent equivalent makes
registerInterfaceraiseTypeError— without this adapter the add-on does not install at all.Named themes in the registry
interfaces.py—ISCVLTThemeDefinition:id,title, plus the themesettings.
theme_settingsis derived by excluding the metadata fields, sofuture non-colour variables need no code change.
profiles/default/registry/sc.voltolighttheme.themes.xml— the default theme.utils/themes.py— CRUD over the theme records, including the duplicatehelper and the guard refusing deletion of the default theme.
vocabularies/themes.py—sc.voltolighttheme.themes.@controlpanels/themescontrolpanels/themes.py— a collection-shaped control panel.plone.restapialready routes sub-paths intoadd/get/update/delete, so no custom service was needed — only its ownISerializeToJson(
serializers/controlpanels.py), since the default reads a single prefixthrough
registry.forInterface.permissions.zcml+rolemap.xml— a dedicated permission for managing themes.Behaviors
behaviors/header.py,behaviors/footer.py— our own site-header,intranet-header and footer behaviors.
behaviors/theme.py—sc.voltolighttheme.themeselector, enabled onPlone Site so nothing is ever left without a theme, and available on any type
for per-section overrides.
serializers/fields.py— the theme field serializer resolves the selectedtheme's settings on read, so a consumer gets the resolved values rather than
just a token.
Note on subclassed behaviors:
plone.autoformmerges same-named fieldsets byappending, so a child that re-lists inherited fields makes
z3c.formraiseValueError("Duplicate name", …)— surfacing as a 500 on@types/<type>.Child fieldsets list only their own fields and use
order_afterto restore theintended order.
Frontend
inheritaction / reducer / types — the values read from the@inheritexpander now live in their own store slice. Volto resets
content.databeforere-fetching, which was blanking every inherited value and causing a visible
flicker in the header and footer when leaving an edit form.
Theming.tsx— applies the resolved theme by overriding the colour customproperties for the whole page.
useLiveData.ts— maps behavior data to live values, so unsaved edits arepreviewed while the form is open.
Controlpanels/Themes/Themes.tsx— the control panel UI: add, edit,duplicate from an existing theme, delete. The default theme is editable like
any other; only its deletion is refused.
helpers/themeStyles.ts,helpers/themesControlpanel.ts— the resolutionand control-panel helpers, both unit-tested.
types/theme.ts— the block styling themes are typed._root.scssbridges--header-foreground-colorviavar(--header-foreground-color, var(--primary-foreground-color)), because--header-foregroundis read by upstream VLT and could not be renamed withthe field. The bridge is also order-independent, unlike a direct override.
Renames
header_foreground→header_foreground_color(regular naming).theme_colors→theme_settings.No upgrade step: no site runs this yet.
Tests
the control panel, the serializers, the vocabulary and the theme utils.
The suite was reorganised to mirror the package layout.
useLiveData,themeStyles,themesControlpanelandconfig/settings.Not verified
None of this has run against a live stack. The control panel UI also has no
rendering tests — the pencil icon, the duplicate action and the toolbar
formatting are verified against Volto's source and at the helper level, but
nothing exercises the rendered component.
Todo
kitconcept.voltolightthemeReview checklist