Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## 0.13.1 — 2026-10-01

Fixes found while rebuilding a real club website on a Bricks 2.4.2 site. See the [release notes](docs/RELEASE-0.13.1.md).

- **Forms save again.** Layouts with a form (contact, hero with form, login, coming soon) were refused by Bricks' `set-page-elements`: the submit button used `submitButtonPadding`, a control Bricks 2.4 does not have. The padding now comes from the `bs-form` class.
- **Light text on dark sections, on every page.**
- Bricks prints class controls as `.bs-title.brxe-heading`, as strong as the dark-section rule.
- When a dark section (a cover hero, a dark header) came before the first title, titles stayed dark on the dark background.
- Surface rules now outrank Bricks' class rules, wherever Bricks prints them.
- **Classes are written the way Bricks stores them.** Saving a class, Bricks moves `gap`, `padding`, `grid-column` and similar declarations into controls, splits selector lists and puts at-rules first. Affected classes reported "another version on the site" forever, and the class update ended "not verified". Two of those conversions also dropped CSS:
- the eyebrow's gap (Bricks' `_gridGap` is not printed on text);
- the tabs' gap.
- **No false warnings.** Saving a page no longer reports "keeps the site's definition" for classes that differ only in how Bricks laid out their CSS.
- **Tests** check every layout for controls Bricks rejects or does not render, for CSS Bricks rewrites, and for override rules weaker than Bricks' own class rules.

## 0.13.0 — 2026-10-01

Mobile menus, carousels, motion and class updates. See the [release notes](docs/RELEASE-0.13.0.md).
Expand Down
76 changes: 76 additions & 0 deletions docs/RELEASE-0.13.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# BricksSnap 0.13.1: Fixes from a real rebuild

We rebuilt a real club website, a German heritage society's homepage, with BricksSnap layouts on a Bricks 2.4.2 site. The page used:

- the club's own copy and photos;
- a cover hero, about, timeline, gallery and board sections;
- an FAQ accordion and a contact form.

Saving it showed four problems that the tests and the earlier live checks had missed. All four are fixed and covered by new tests.

## 1. Forms save again

Bricks refused every layout with a form:

> Invalid value for "settings.submitButtonPadding". Expected a registered setting for the 'form' element.

Bricks 2.4 has no padding control for the submit button. The padding now comes from the `bs-form` class, so the button looks the same.

Affected layouts: Hero with enquiry form, Contact with form, Centred form, Sign in, Sign in split, and Coming soon split.

## 2. Light text on dark sections, on every page

Bricks prints a class's controls as `.bs-title.brxe-heading`. That rule is as strong as the rule that turns titles light on dark sections, so whichever came later on the page won. When a dark section came before the first title, titles stayed dark on the dark background, for example with a cover hero or a dark header.

Dark and brand-colored sections now use rules that outrank Bricks' class rules wherever Bricks prints them. Some rules also overrode a class from a parent at only equal strength:

- the bento feature card;
- logo tiles;
- the rating card;
- button rows in heroes.

They now outrank it too.

## 3. Classes are written the way Bricks stores them

A probe class on the test site showed what Bricks 2.4.2 does when it saves a class's custom CSS:

- **Controls.** It moves declarations of the class's own rule into controls:
- `display`, `gap`, `row-gap`, `column-gap`, `padding`, `width`;
- `grid-column`, `grid-row`, `opacity`, `visibility`;
- some typography.
- **Selector lists.** It splits them into one rule per selector.
- **Order.** It puts at-rules (`@media`, `@keyframes`) first.

BricksSnap compared the stored version with its own, so these classes showed "another version on the site" forever, and **Update BricksSnap classes** ended "not verified". Two conversions also lost CSS:

- `gap` became `_gridGap`, which Bricks does not print on text, so the eyebrow lost its spacing;
- the tabs' row gap was a control Bricks does not print on nested tabs.

BricksSnap now writes these classes in Bricks' own form. On sites with an earlier version, **Update N BricksSnap classes** in Staging brings them up to date.

## 4. No false warnings when saving

Saving a page compared classes strictly and warned "keeps the site's definition" for classes that differed only in how Bricks laid out their CSS. That check now ignores layout like the other comparisons.

## Verified

- **Automated:** 267 tests. New checks across all 92 layouts in every style:
- no control Bricks rejects;
- no class control Bricks does not print for the element it is on;
- no class CSS that Bricks rewrites when it saves a class;
- no rule that overrides another class at only equal strength.
- **Live on the Bricks 2.4.2 test site:** the rebuilt page with 227 elements and 97 classes.

| Check | Result |
|---|---|
| Saving | page verified, no warnings |
| Class update | verified; 0 of 97 classes differ afterwards |
| Cover hero | light title on the dark photo |
| Eyebrow spacing | rendered (`gap: 10px`) |
| Mobile menu and FAQ accordion | open as expected |
| Images and JavaScript | no broken images, no JavaScript errors |

## Known limit

Bricks loads web fonts only from its own typography settings. Add the kit's fonts under **Bricks → Settings → Custom fonts** or in a theme style; otherwise visitors see the fallback fonts. See [Studio](STUDIO.md).
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "brickssnap",
"version": "0.13.0",
"version": "0.13.1",
"scripts": {
"dev": "next dev --webpack",
"dev:local": "node scripts/dev-local.mjs",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/bricks-validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const BRICKS_ELEMENT_NAMES = new Set<string>(Object.keys(schema.elements)
// _hidden carries the classes Bricks' nestable elements rely on (tab-title, accordion-content-wrapper, …).
const metaControls = new Set(["_cssGlobalClasses", "_conditions", "_interactions", "_hideElementBuilder", "_hideElementFrontend", "_attributes", "_hidden"]);
// Controls the published schema leaves out, verified against Bricks 2.4.2's rendered output.
const verifiedControls: Record<string, string[]> = { "nav-nested": ["ariaLabel", "mobileMenu"] };
export const verifiedControls: Record<string, string[]> = { "nav-nested": ["ariaLabel", "mobileMenu"], image: ["_objectFit", "_objectPosition"] };
const controlSets = new Map(Object.entries(schema.elements).map(([name, element]) => [name, new Set([...schema.commonControls, ...element.controls, ...(verifiedControls[name] ?? []), ...metaControls])]));

// A Bricks id is always a 6-char lowercase alphanumeric string.
Expand Down
2 changes: 1 addition & 1 deletion src/lib/kit/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ function formCss(selector: string, s: Settings): string {
part("label", declarations("_typography", s.labelTypography)),
part(":is(input, textarea)", [...declarations("_typography", s.fieldTypography), ...(color(s.fieldBackgroundColor) ? [`background-color: ${color(s.fieldBackgroundColor)}`] : []), ...sides("padding", s.fieldPadding), ...(fieldBorder ? declarations("_border", fieldBorder) : [])]),
part(".form-group", sides("margin", s.fieldMargin)),
part(".bricks-button", [...declarations("_typography", s.submitButtonTypography), ...(color(s.submitButtonBackgroundColor) ? [`background-color: ${color(s.submitButtonBackgroundColor)}`] : []), ...sides("padding", s.submitButtonPadding), ...(submitBorder ? declarations("_border", submitBorder) : [])]),
part(".bricks-button", [...declarations("_typography", s.submitButtonTypography), ...(color(s.submitButtonBackgroundColor) ? [`background-color: ${color(s.submitButtonBackgroundColor)}`] : []), ...(submitBorder ? declarations("_border", submitBorder) : [])]),
].filter(Boolean).join("\n");
}

Expand Down
6 changes: 4 additions & 2 deletions src/lib/kit/sections/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ export function commonClasses(r: ResolvedKit): ClassLibrary {
"bs-info__value": { ...x.type({ size: "text-m", weight: "500", color: "heading", decoration: "none" }), _margin: { top: "0", bottom: "0" } },

// Tabs: layout classes plus one look per part.
"bs-tabs": { _display: "flex", _direction: "column", _rowGap: x.v("space-l"), _alignItems: "stretch", _width: "100%" },
// Bricks lays tabs out as a column; tabs-nested has _gap but no row gap or direction control.
"bs-tabs": { _display: "flex", _gap: x.v("space-l"), _alignItems: "stretch", _width: "100%" },
"bs-tabs__menu": { _direction: "row", _flexWrap: "wrap", _rowGap: "4px" },
"bs-tabs__menu--pill": merge(x.bg("surface"), x.line("1px", "border"), x.round("999px"), x.pad("4px"), { _columnGap: "4px", _width: "auto", _alignSelf: "center" }),
"bs-tabs__menu--line": { _columnGap: x.v("space-m"), _width: "100%", _border: { width: { top: "0", right: "0", bottom: "1px", left: "0" }, style: "solid", color: x.color("border") } },
Expand All @@ -155,7 +156,8 @@ export function commonClasses(r: ResolvedKit): ClassLibrary {
"bs-acc__q": merge(x.type({ size: "text-l", weight: "heading-weight", ls: "heading-tracking", lh: "1.35", color: "heading" }), { _margin: { top: "0", bottom: "0" } }, x.font("bs-acc__q", "heading", " text-wrap: balance;")),
"bs-acc__icon": merge(x.type({ size: "22px", color: "link" }), {
_flexShrink: "0", _cssTransition: "transform .2s ease",
_cssCustom: `.bs-acc__item.brx-open .bs-acc__icon, .bs-acc__title[aria-expanded="true"] .bs-acc__icon { transform: rotate(45deg); }`,
// One rule per selector: Bricks splits selector lists when it saves a class.
_cssCustom: `.bs-acc__item.brx-open .bs-acc__icon { transform: rotate(45deg); }\n.bs-acc__title[aria-expanded="true"] .bs-acc__icon { transform: rotate(45deg); }`,
}),
"bs-acc__content": { _widthMax: "720px", _padding: { top: "0", right: "0", bottom: x.v("space-m"), left: "0" } },
};
Expand Down
9 changes: 5 additions & 4 deletions src/lib/kit/sections/conversion-plus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ function convertPlusClasses(r: ResolvedKit) {
"bs-price-tiles": { _display: "grid", _gridTemplateColumns: "repeat(3, minmax(0, 1fr))", "_gridTemplateColumns:tablet_portrait": "repeat(2, minmax(0, 1fr))", "_gridTemplateColumns:mobile_landscape": "minmax(0, 1fr)", _gridGap: x.v("space-s"), _width: "100%" },

// Testimonials
"bs-wall": { _display: "block", _width: "100%", _cssCustom: `.bs-wall { columns: 3 260px; column-gap: ${x.v("space-m")}; }\n.bs-wall > * { break-inside: avoid; margin-bottom: ${x.v("space-m")}; }` },
"bs-wall": { _display: "block", _width: "100%", _columnGap: x.v("space-m"), _cssCustom: `.bs-wall { columns: 3 260px; }\n.bs-wall > * { break-inside: avoid; margin-bottom: ${x.v("space-m")}; }` },
"bs-wall__rating": merge(x.bg("primary"), x.round("radius-l"), x.pad("space-card"), x.type({ color: "on-primary" }), {
_display: "flex", _direction: "column", _rowGap: "8px", _alignItems: "flex-start",
_cssCustom: `.bs-wall__rating .bs-stars__icon { color: ${x.v("on-primary")}; }`,
_cssCustom: `.bs-wall__rating.bs-wall__rating .bs-stars__icon { color: ${x.v("on-primary")}; }`,
}),
"bs-wall__score": merge(x.type({ size: "text-display", weight: "heading-weight", ls: "heading-tracking", lh: "1", color: "inherit" }), { _margin: none }, x.font("bs-wall__score", "heading")),
"bs-wall__meta": merge(x.type({ size: "text-s", color: "inherit" }), { _margin: none, _opacity: "0.88" }),
Expand Down Expand Up @@ -59,9 +59,10 @@ function convertPlusClasses(r: ResolvedKit) {

// Logos
"bs-logo-tiles": { _display: "grid", _gridTemplateColumns: "repeat(auto-fit, minmax(160px, 1fr))", _gridGap: x.v("space-s"), _width: "100%" },
"bs-logo-tile": merge(x.bg("surface-alt"), x.round("radius-m"), x.pad("space-s"), { _display: "flex", _alignItems: "center", _justifyContent: "center", _heightMin: "96px", _cssCustom: ".bs-logo-tile .bs-logos__name { white-space: normal; text-align: center; }" }),
"bs-logo-tile": merge(x.bg("surface-alt"), x.round("radius-m"), x.pad("space-s"), { _display: "flex", _alignItems: "center", _justifyContent: "center", _heightMin: "96px", _cssCustom: ".bs-logo-tile.bs-logo-tile .bs-logos__name { white-space: normal; text-align: center; }" }),
"bs-marquee": { _width: "100%", _overflow: "hidden", _cssCustom: ".bs-marquee { mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent); }" },
"bs-marquee__track": { _display: "flex", _direction: "row", _width: "max-content", _cssCustom: ".bs-marquee__track { animation: bs-marquee 32s linear infinite; }\n.bs-marquee:hover .bs-marquee__track { animation-play-state: paused; }\n@keyframes bs-marquee { to { transform: translateX(-50%); } }\n@media (prefers-reduced-motion: reduce) { .bs-marquee__track { animation: none; } }" },
"bs-marquee__track": { _display: "flex", _direction: "row", _width: "max-content", // At-rules first, as Bricks stores them.
_cssCustom: "@keyframes bs-marquee { to { transform: translateX(-50%); } }\n@media (prefers-reduced-motion: reduce) { .bs-marquee__track { animation: none; } }\n.bs-marquee__track { animation: bs-marquee 32s linear infinite; }\n.bs-marquee:hover .bs-marquee__track { animation-play-state: paused; }" },
"bs-marquee__group": { _display: "flex", _direction: "row", _alignItems: "center", _columnGap: "clamp(40px, 6vw, 80px)", _padding: { top: "8px", right: "clamp(40px, 6vw, 80px)", bottom: "8px", left: "0" } },
};
}
Expand Down
2 changes: 1 addition & 1 deletion src/lib/kit/sections/header-hero-plus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function heroPlusClasses(r: ResolvedKit) {
return {
"bs-hero-bento": { _display: "grid", _gridTemplateColumns: "minmax(0, 1.2fr) minmax(0, 1fr)", _gridGap: x.v("space-s"), _width: "100%" },
// Bricks has no grid span control on image elements, so the span lives in class CSS.
"bs-hero-bento__main": merge(x.round("radius-l"), { _width: "100%", _height: "100%", _heightMin: "340px", _objectFit: "cover", _cssCustom: ".bs-hero-bento__main { grid-row: span 2; }" }),
"bs-hero-bento__main": merge(x.round("radius-l"), { _width: "100%", _height: "100%", _heightMin: "340px", _objectFit: "cover", _cssCustom: ".bs-hero-bento .bs-hero-bento__main { grid-row: span 2; }" }),
"bs-hero-bento__tile": merge(x.round("radius-l"), x.pad("space-card"), { _display: "flex", _direction: "column", _justifyContent: "flex-end", _rowGap: "6px", _heightMin: "160px" }),
"bs-hero-bento__tile--primary": merge(x.bg("primary"), x.type({ color: "on-primary" })),
"bs-hero-bento__tile--soft": merge(x.bg("primary-soft"), x.type({ color: "heading" })),
Expand Down
2 changes: 1 addition & 1 deletion src/lib/kit/sections/header-hero.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function heroClasses(r: ResolvedKit) {
const x = sx(r);
return {
// The hero's buttons get extra room below the lead, most noticeable on phones.
"bs-hero": { _cssCustom: ".bs-hero .bs-btn-row { margin-top: 8px; }" },
"bs-hero": { _cssCustom: ".bs-hero.bs-hero .bs-btn-row { margin-top: 8px; }" },
"bs-hero__content": { _display: "flex", _direction: "column", _rowGap: x.v("space-m"), _alignItems: "flex-start" },
"bs-hero__visual": { _position: "relative", _width: "100%" },
"bs-hero__badge": merge(x.bg("surface"), x.round("radius-m"), x.pad("12px", "16px"), x.shadow("16px", "40px", "-16px"), {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/kit/sections/more-plus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ function morePlusClasses(r: ResolvedKit) {
"bs-post-line__img": merge(x.round("radius-m"), { _width: "100%", _aspectRatio: "4/3", _objectFit: "cover", "_order:mobile_landscape": "-1" }),

// Galleries
"bs-masonry": { _display: "block", _width: "100%", _cssCustom: `.bs-masonry { columns: 3 150px; column-gap: ${x.v("space-s")}; }` },
"bs-masonry": { _display: "block", _width: "100%", _columnGap: x.v("space-s"), _cssCustom: ".bs-masonry { columns: 3 150px; }" },
"bs-masonry__img": merge(x.round("radius-m"), { _display: "block", _width: "100%", _objectFit: "cover", _margin: { top: "0", bottom: x.v("space-s") }, _cssCustom: ".bs-masonry__img { break-inside: avoid; }" }),
"bs-gbento": { _display: "grid", _gridTemplateColumns: "repeat(4, minmax(0, 1fr))", "_gridTemplateColumns:mobile_landscape": "repeat(2, minmax(0, 1fr))", _gridAutoRows: "clamp(130px, 15vw, 220px)", _gridGap: x.v("space-s"), _width: "100%" },
"bs-gbento__cell": { _overflow: "hidden" },
Expand Down
7 changes: 4 additions & 3 deletions src/lib/kit/sections/more.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,16 @@ function moreClasses(r: ResolvedKit) {
"bs-gallery": { _display: "grid", _gridTemplateColumns: "repeat(3, minmax(0, 1fr))", "_gridTemplateColumns:mobile_landscape": "repeat(2, minmax(0, 1fr))", _gridAutoRows: "clamp(150px, 20vw, 280px)", _gridGap: x.v("space-s"), _width: "100%" },
"bs-gallery__item": merge(x.round("radius-m"), { _width: "100%", _height: "100%", _objectFit: "cover", _overflow: "hidden" }),
// Bricks has no grid span control on image elements, so the span lives in class CSS.
"bs-gallery__wide": { _cssCustom: ".bs-gallery__wide { grid-column: span 2; }" },
// Inside the grid's rule: Bricks turns `.bs-gallery__wide { grid-column }` into a control.
"bs-gallery__wide": { _cssCustom: ".bs-gallery .bs-gallery__wide { grid-column: span 2; }" },
"bs-contact": { _display: "grid", _gridTemplateColumns: "minmax(0, 0.9fr) minmax(0, 1.1fr)", "_gridTemplateColumns:tablet_portrait": "minmax(0, 1fr)", _gridGap: x.v("space-xl"), _alignItems: "start", _width: "100%" },
"bs-contact__info": { _display: "flex", _direction: "column", _rowGap: x.v("space-m") },
"bs-form-card": merge(x.bg("surface"), x.line("1px", "border"), x.round("radius-l"), x.pad("space-card"), { _display: "flex", _direction: "column", _rowGap: x.v("space-s"), _width: "100%" }),
"bs-center": merge({ _display: "flex", _direction: "column", _rowGap: x.v("space-m"), _alignItems: "center", _widthMax: "640px", _margin: { left: "auto", right: "auto" } }, x.type({ align: "center" })),
"bs-code": merge(x.type({ size: "clamp(88px, 60px + 8vw, 160px)", weight: "heading-weight", ls: "heading-tracking", lh: "1", color: "link" }), { _margin: { top: "0", bottom: "0" } }, x.font("bs-code", "heading")),
"bs-screen": { _heightMin: "min(80vh, 760px)", _justifyContent: "center" },
"bs-form": { _width: "100%" },
// Bricks 2.4 has no padding control for the submit button.
"bs-form": { _width: "100%", _cssCustom: ".bs-form .bricks-button { padding: 14px 24px; }" },
};
}

Expand All @@ -134,7 +136,6 @@ export function form(ctx: Ctx, fields: Array<{ type: string; label: string; requ
submitButtonBackgroundColor: x.color("primary"),
submitButtonTypography: { "font-size": "16px", "font-weight": "600", color: x.color("on-primary") },
submitButtonBorder: { radius: x.sides(x.v("radius-btn")) },
submitButtonPadding: { top: "14px", right: "24px", bottom: "14px", left: "24px" },
});
}

Expand Down
Loading
Loading