Conversation
createThemeDefinition maps 14 semantic --theme-* properties onto
--block-theme-{name}-* tokens, but _root.scss defined only 10 of them for
the default theme and 9 for brand. A var() with no definition and no
fallback is invalid at computed-value time, so the whole declaration is
dropped rather than degrading — which took out six focus outlines in
_listing.scss, leaving keyboard focus invisible on listing cards.
Measured after the fix: focus on a carousel slide now paints
"solid 2px rgb(0,0,0)" in light and "solid 2px rgb(255,255,255)" in dark,
with none when unfocused.
Also corrects a --theme-low-contrast-foreground reference in
_heroBlock.scss that was missing its -color suffix, and gives the brand
theme a border-width outside high-contrast mode.
Adds a test guarding the contract, because nothing else can catch this:
there is no typecheck in CI, stylelint does not resolve custom properties
across files, and a story renders happily without an outline. It reads the
:root block specifically — a token declared only under
[data-theme='high-contrast'] is still undefined in light and dark, which is
exactly what brand's border-width was. Verified by reintroducing both
original defects and confirming each one fails it.
Refs #22
Every string in the add-on was Portuguese, supplied as a defaultMessage with no catalogue behind it — all four .po files were empty boilerplate. Some were not even messages: listing variation titles, block titles and sixteen carousel aria-labels were plain strings, and useHeroBlockContent had no intl at all. All of it now goes through react-intl with stable ids and English default messages. Block and listing-variation titles are registered for extraction in src/index.ts, because Volto translates those with formatMessageWithFallback, which uses the string itself as the message id — without that registration they never reach the catalogue. pnpm i18n extracts 147 messages. pt_BR is complete and validates with msgfmt --check; es and de stay empty, since inventing translations is not mine to do. Three defects surfaced while doing this and are fixed here: - documentByline presented itself as a weather block. Its schema title read "Previsão do Tempo", contradicting its registration title, and its exported interface declared location and measure instead of the three fields the schema has. Its message ids were the Portuguese sentences themselves, which breaks extraction. Its boolean fields were also marked required, one of them defaulting to false, making the block unsaveable in a strict form. - heroBlock.fullWidth was unreachable. Both views branch on it to render full-bleed when there is no image, but it appeared in no fieldset, so no editor could set it. It is now in the Layout fieldset. - The carousel and highlight listing variations formatted dates as pt-BR regardless of the active language. Both now use the locale from react-intl. Neither had an accent, which is why a grep for Portuguese missed them and the message extraction found them. Sample copy in the stories is now generic English; the named third parties it previously carried were removed. Refs #22
Stories rendered block views bare, so --theme-* was undefined in every one of them and Storybook never exercised the block theme system at all. HeroBlock alone carries 30 references to it, none of which resolved. That is how the missing focus outlines went unnoticed. The decorator adds three toolbar globals — colour mode (light, dark, high contrast), block theme (Primary, Brand) and site theme, seeded from the ISCVLTThemeDefinition fixtures the control panel stories already use. It reuses createThemeDefinition rather than restating the 14-property mapping, so a theme added to the block config needs no change here. The colour mode has to be applied in two places, which is worth recording. Upstream registers six tokens with @Property { syntax: '<color>' }. A registered custom property is computed eagerly at the element that declares it, so its light-dark() resolves once at :root and inherits as a flat colour; an unregistered one resolves wherever it is used. Storybook's preview resets color-scheme below <html>, so setting the mode on the root alone leaves the unregistered half stuck, and setting it on a wrapper alone leaves the registered half stuck. Setting both fixes all fourteen probed tokens, matching an isolated render of _root.scss with no Volto present. Every story is also constrained to --default-container-width and centred, so components are seen at the measure they occupy on a page. Stories that genuinely span the viewport opt out with parameters: { fullBleed: true }. Storybook now serves the add-on's public folder at its root, so a story can reference a bundled asset as /images/example.jpg rather than depending on a remote image host at view time. public/images/README.md records the four places a fixture names its image source. Refs #22
Covers the four listing variations and the six navigation components, which between them had no stories at all — the largest gaps in the package. The listing fixtures are shaped like a querystring-search response; the navigation ones already existed and had no importer. Getting them to render turned up the same failure three times, and it is worth stating as a pattern: a component's stylesheet is scoped to an ancestor that only exists on a page, so a story rendering it bare gets none of its styling — and the symptom always looks like missing CSS rather than a missing wrapper. - _listing.scss is scoped to "body .block.listing" and then to each variation, so the templates render inside "block listing <variation>". This is what makes the restored focus outlines observable. - _navigation.scss is scoped to "#navigation.navigation" and "&.scNavigation". The existing decorator supplied the id and the first class but not the second, so nothing matched: the fat menu rendered its content but .submenu-items never became the two-column grid it declares and the links kept the browser's default colour. - The fat menu panel is absolutely positioned against the item that triggers it, so it needs the .desktop-menu list and an item with width. Its open state is signalled by an "active" class, which two decorators had spelled differently. The accessibility controls are the instructive counter-example. Their selectors are unscoped, so their rules did apply — and they still looked unstyled, because the buttons are drawn as light pills for the dark header bar and the story put them on a hardcoded pale background. The hardcoded greys in the header bar, accessibility and dropdown stories are now theme tokens, so they follow the colour mode. Two Portuguese labels left in the header bar mocks are translated; they were story data rather than messages, so neither the extraction nor a grep for accents had found them. Refs #22
A site running this theme gives an editor 42 blocks; the add-on ships four of them and extends two. Nothing showed the other 36, which this theme restyles and is therefore very much its concern. Rather than 36 hand-written story files that would rot against upstream, two stories carry it: - Blocks/Inventory reads config.blocks.blocksConfig at runtime, so it lists what an editor actually has and cannot drift — a block added by a dependency bump appears on its own, labelled by which layer supplies it. - Blocks/Gallery renders each of them through RenderBlocks with sample data, so the whole vocabulary can be seen at once. All 42 render without error. Five had to be fixed to get there: accordion needs data.data with a nested blocks_layout, slateTable needs data.table.rows, and highlight reads state.content.subrequests keyed by block id — all three threw on undefined. search and eventCalendar dispatch thunks on mount, which a plain mock store rejects, so the gallery uses Volto's real-store wrapper. Worth knowing when auditing this: RenderBlocks has its own per-block error boundary and it is the inner one, so a failing block reports "Block error: ..." rather than reaching any boundary the gallery adds. A check that greps for the outer message finds nothing and also counts the failure as rendered content, because Volto's message is long enough to pass a length test. That combination reported a clean run while five blocks were broken. Sample data for the social embeds is taken from each block's own story in @kitconcept/volto-social-blocks rather than invented — those are the URLs the block authors test against. Also adds page-level views through Volto's DefaultView (a Document, a News Item and an Event, the last built from the initial blocks upstream defines for the type), gridBlock at one to four columns, and the separator block's three controls. Stories are split by ownership: a story for a component this package ships sits beside the component, and a story for a component from a dependency lives in src/storybook next to the decorator and the shared fixtures. Refs #22
Three documents under docs/design-system, with a short pointer at the repository root. DESIGN_SYSTEM.md is the manifest. It opens with the three layers that stack here — Volto core, upstream volto-light-theme, this add-on, and a project's overlay — because nearly everything confusing about the theme comes from not knowing which layer you are looking at. It documents what the add-on owns and points at the runtime inventory for what it inherits, rather than repeating a list that goes stale. It also records two constraints that cost real time to find: the six tokens upstream registers with @Property, which cannot re-resolve on a descendant, and the ancestor chains each stylesheet is scoped to. INVENTORY.md carries the exhaustive tables — block schemas, listing variations, every custom property with its resolved light and dark value, the type scale, and where a story lives. GAPS.md carries what is still open: a WCAG contrast audit of the shipped palette, the defects not yet fixed, the coverage not yet built, and fourteen decisions that need a human rather than a patch. Resolved items are deliberately not kept there — the history is in the changelog. Every claim was checked against source or measured in a browser. Two earlier ones were wrong and are corrected here: the scalar typography overrides are exact px-to-rem conversions of upstream's !default values rather than duplicates, and --custom-main-font is read twice by upstream even though nothing in this package uses it. previews/ holds self-contained pages marked up for import into Claude Design. The foundation pages are generated from the stylesheets; the block pages were captured from a Storybook build whose pipeline is not in the repo, so they cannot currently be regenerated — that is recorded in GAPS §5.1 as something to resolve either way. Refs #22
…ypes The three content-type tokens read `--event-color-override` and friends, which nothing writes: a theme's `event_color_light` setting reaches the page as `--event-color-light`. The control panel's new content-type fields therefore saved values that changed nothing. They now read the properties a theme sets, through `light-dark()` like every other colour, so they follow the colour mode as well. The tests checked that a field derives a custom property, but not that any stylesheet reads it — which is exactly the half that was wrong. `themeStyles` now checks both ends: that the pinned field list still matches the backend schema, and that `_root.scss` reads every property the mapping derives. The pinned list was itself stale, claiming to hold every colour field while listing 20 of 38. Both guards were mutation-tested: restoring the old property name and dropping a field from the list each turn the suite red. Refs #22
Both WCAG AA failures are fixed, so §4.4 records what fixed them and why `--brand-color-darker` had to exist rather than listing them as open. The per-project overlay section described seven flat fields; the schema now serves 38 as `_light`/`_dark` pairs, which is what keeps a themed site in both colour modes. The base ramp is ten values, and `2xs` has a line-height. GAPS.md was renumbered when its resolved sections were removed, so every cross-reference in DESIGN_SYSTEM.md, INVENTORY.md and the preview pages is re-pointed. The identical `narrow` and `default` container widths came back as an open gap: they were removed while still true. The preview pages carried the old failing swatches and their "FAILS AA" flags, and six of them cited GAPS sections that never matched — the block pages pointed at §7 for the two-place colour-scheme trick, which lives in DESIGN_SYSTEM §4.3. `block-themes.html` also had a footer sentence left half-rewritten. Also fixes a broken table in DESIGN_SYSTEM §7, where the `_navigation.scss` row sat below a blank line and rendered as its own one-row table. Refs #22
Volto is a webpack application, not a publishable component package, and the
design-system converter bundles with esbuild. Five things bridge that gap; none
of them forks the converter, and `.design-sync/NOTES.md` explains each one with
the evidence behind it.
`build-overlay.py` builds a synthetic node_modules encoding Volto's alias scheme
— `webpack-relative-resolver` rewrites `@plone/volto/X` to `@plone/volto/src/X`
at build time, which plain node resolution cannot do. That alone took the build
from 121 unresolved imports to none.
`build-icons.py` generates a JS twin per SVG carrying the `{attributes, content}`
shape Volto's `Icon` reads, because esbuild loads `.svg` as a data URL and `Icon`
then renders an empty `<svg>`. `build-css.sh` compiles a standalone stylesheet,
since the add-on ships SCSS that webpack injects at runtime.
`src/storybook/` gains the runtime context a standalone bundle lacks: a curated
component surface (the package's only export is `applyConfig`), razzle's
build-time globals, a seeded config registry, and the provider chain the
storybook decorators supply. `createThemeDefinition` moves to `config/blockThemes`
— re-exported from `config/blocks`, so every import is unchanged — because
importing it from there registers every block and drags in the slate editor.
The icons in `ThemeToggle` and `HoverReaderControls` are imported through the
package name rather than a relative path, the form `.storybook/preview.jsx`
already used: a relative import cannot be rerouted by the overlay.
`packages/*/dist` is ignored by prettier and eslint — it is build output, and the
lint globs matched it once something finally emitted there.
Refs #22
npm ignores `.gitignore` entirely once an `.npmignore` exists, and this one listed a single file. Everything gitignored was therefore being packed — including the `dist/` declarations and root `index.d.ts` the design-system export emits, 155 files that exist only to feed the converter. Stories, tests and their fixture assets go too: they are consumed by Storybook and by the export, never by a project installing this package. 404 files (1242 KB) -> 192 files (509 KB). Refs #22
There was no `_documentByLine.scss` anywhere in `src/theme/`, so the block started flush against the viewport edge on a page while every other block sat in the centred content column. In isolation it looked fine, because a story gives it a padded container. The three fields now render inline at the small type step, separated by a dash, with author names comma-separated. The byline also reads author first, then published, then modified, and its rows are paragraphs rather than nested `Container` elements. Closes GAPS §2.1. Refs #22
The fixtures pointed at `picsum.photos`: arbitrary third-party photographs with no licence trail, fetched over the network every time a story rendered. Fourteen photographs of Brasilia now ship in `src/storybook/images/`, imported rather than linked — webpack inlines them for Storybook, esbuild inlines them for the Claude Design export, so stories render offline and identically every time. `imageScales()` shapes an imported URL the way Plone's REST API shapes an image field, splitting it across `base_path` and `download` as Volto's `Image` expects. It declares an empty `scales` map, so nothing advertises widths that no file behind it actually has — the previous fixtures claimed 1200x800 with 3:2 scales while the theme renders every card at 16:9. `optimize.sh` holds the set under a weight budget: these inline as base64 in the export, where a megabyte on disk is closer to 1.4 in a preview bundle. 2702 KB -> 1759 KB, with the sRGB profiles preserved. Story-support modules are excluded from the package alongside the photographs they import, and the `README.md` ignore rule is scoped to the package root — a bare rule was hiding `src/storybook/README.md`, which `DESIGN_SYSTEM.md` links to, from git entirely. Refs #22
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.
Prepares the repository to be read as a design system — by a person, and by tooling like Claude Design.
The work started as documentation. Writing it honestly meant rendering things, and rendering them turned up a set of defects that had been invisible precisely because nothing rendered them. Roughly half of this PR is those fixes.
What this changes
Storybook goes from 13 stories to 103, and from a handful of isolated components to a description of the whole system: every block an editor can reach, each listing variation, the navigation and fat menu, and complete pages for a Document, a News Item and an Event.
Every user-facing string is translatable. All of them were Portuguese
defaultMessages with no catalogue behind them — and some were not messages at all, just hardcoded strings inaria-labels and block titles. There are now 147 extracted messages withpt_BRcomplete.The design system is documented under
docs/design-system/, from measurement rather than assumption.The defects this surfaced
createThemeDefinitionmaps 14--theme-*properties;_root.scssdefined 10. Avar()with no definition and no fallback is invalid at computed-value time, so sixoutlinedeclarations in_listing.scsswere dropped entirely rather than degrading. Now fixed, and guarded by a test that fails if the contract breaks again.documentBylinepresented itself as a weather block — its schema title read "Previsão do Tempo", contradicting its own registration title, and its exported interface declaredlocationandmeasure.heroBlock.fullWidthwas unreachable. Both views branch on it, but it appeared in no fieldset, so no editor could set it.pt-BRregardless of the active language.The pattern worth reviewing
Four times, a component looked unstyled in Storybook and was in fact fine — the story simply rendered it outside the DOM context its stylesheet is scoped to.
_listing.scssneedsbody .block.listing;_navigation.scssneeds#navigation.navigation.scNavigation; a themed block needs a wrapper supplying--theme-*.The accessibility controls are the instructive counter-example: their selectors are unscoped, so their rules did apply, and they still looked wrong — the buttons are drawn as light pills for the dark header bar, and the story put them on a hardcoded grey. The CSS was never the problem; the ground was.
This is recorded in
DESIGN_SYSTEM.md§7 as a rule for writing stories, because it will happen again.What is deliberately not fixed
GAPS.md§7 lists fourteen decisions that need a person rather than a patch. Two are worth flagging here:#f4822con a near-white ground — accent text on elevated cards in the default theme and default colour mode, and the footer accent in dark mode. The footer one is fixable today with an existing token; the other needs a third brand shade that does not exist. Neither was applied, because both change shipped brand appearance and both depend on whether#f4822cis a deliberate default at all.themeStyles.tswrites it flat rather than as a-light/-darkpair.Known rough edges
picsum.photos— arbitrary third-party photographs with unknown licence, and a network dependency at view time.public/images/is wired and served, ready for replacements of known provenance.docs/design-system/previews/were generated from a Storybook capture whose pipeline is not in the repo, so they cannot currently be regenerated. Either the generator gets committed or those four files should go; the foundation pages are hand-authored from the stylesheets and do not have this problem.Reviewing this
The six commits are meant to be read in order; each has a message explaining why, not just what.
docs/design-system/DESIGN_SYSTEM.mdis the best entry point for the system itself, andGAPS.mdfor what is still open.Refs #22