Recent Activities
This page shows what are we working on.
-
head
-
Minicart update in progress.... c3d18b
-
Minicart update in progress... (#2) 55e7b9
-
INstaller. Create dummy CMS block header_panel_info to allow user add custom content (#2) f70d58
-
Adjust minicart counter (#2) 03e843
-
BCB homepage: Added brand logos section b905f1
-
BCB homepage: Added testimonials section 2ab2cf
-
Update newsletter styles: adjust input border and add consent positioning (#2) 02d37d
-
Header. Update search behaviou and look of the result dropown. 993432
-
BCB homepage: Added FAQ a0bfde
-
Add focused search styles. d80f88
-
BCB homepage: added new sections 875068
-
BCB homepage: added ECI and full-width banner d40cfa
-
Top navigation added. 8fa9a3
-
Remove color from menu links dec081
-
Clone currency and store switchers in header slideout menu for improved functionality de36e3
-
Enhance header and newsletter styles for improved layout and responsiveness in header slideout menu. b63dd0
-
Installer. Update header slideout menu styles. 825795
-
Update footer top content HTML to enhance newsletter section styling.
Don't set font familiy for header in Page Builder ba0b81 -
BCB homepage WIP b228af
-
Basic navpro slideout styles 75ac0e
-
Refactor header slideout menu and newsletter styles for improved layout and consistency 3b42ff
-
Header slideout menu... d4f174
-
Listing styles for the list mode and old price update. cbff61
-
Update README.md to clarify usage of `with-bottom-divider` CSS class and provide detailed instructions 1b9ea5
-
Pagination and make with-bottom-divider general. 37ea17
-
Added list banner 5 config; updated installer, moved it to json a43985
-
Add new layout configuration and remove easybanner references; update footer content and styles 87165b
-
Refactor easybanner configuration and installer for product listing banners 8b1d0d
-
Installer. Listing grid banner added. 914c01
-
Add swatches variables and layout styles d59581
-
Update positioning for filter title and content in layered navigation 8cbd21
-
Add image dimensions for product listings and related widgets in view.xml 6985cd
-
layout and styles for category view, product toolbar, shop by button 08fd27
-
Add layered navigation styles and update imports 944ff1
-
Keep working on prodcut listing and category page. 411088
-
Keep working on product listing toolbar. ebcfac
-
Working on product listing and toolbar look. 1ab39d
-
Add Cooper* font 132906
-
Add listing item styles and variables e725b5
-
Product lisnting in progress - grey bg and border radius. 4e7c35
-
Add new variable files for colors, typography, header, footer, icons, navigation, popover, minisearch, and gradient ba122c
-
Refactor footer and newsletter styles for improved consistency and responsiveness d831e4
-
Footer top newsletter - some updates. 452ca5
-
Add footer top content block and styles for newsletter integration (WIP) 128131
-
Footer bottom. 8d58ea
-
Add footer content block and styles for enhanced footer layout 4d6b1b
-
Header. Hide menu on medium screens. Update search look on monile. e0146d
-
Installer. Add navigation slideout menu a89fd5
-
Add EasySlide functionality for header panel with custom slides and styles e55b0b
-
Add minisearch layout and styles to enhance search functionality 5c5aec
-
Update config.xml and LESS files for theme adjustments and styling improvements of search field 09f88d
-
Add Lexend font and related styles to the theme 0100e8
-
-
1.5.56
-
1.5.55
-
head
-
1.2.1
-
head
-
Merge pull request #81 from breezefront/feat/editor-chat-panel
feat(content-builder): chat panel in the editor for building and refining a page (#64) 0932fb -
fix(content-builder): ground every request in the live working copy
pageContext() only supplies the page's title and id, so a first chat
message had no representation of the page's actual content, and manual
edits made between AI replies were silently overwritten by the last
frozen AI document. Every request now seeds or refreshes its document
turn from contentBuilder.getComponents() instead. Also drops historical
image attachments from the request — maxBytes is sized for one
screenshot under post_max_size, and replaying every prior attachment on
each follow-up can push the aggregate body past that limit. 9ca7e3 -
fix(content-builder): give the message textarea an accessible name b48efb
-
refactor(content-builder): share the chat loop between the modal and the editor panel (#64)
Extracts turn storage, chat-bubble rendering, and copy-to-clipboard into
chat-core.js, used by both chat.js's field-targeted modal and
bcb-chat-panel.js's docked panel. The request/response cycle stays local
to each caller — they talk to different skills with different payload
and response shapes, and forcing that through one function would trade
duplication for a worse, branchier abstraction.
Fixes two bugs the duplication had introduced in bcb-chat-panel.js: the
Copy button was never bound (only chat.js's copy of the handler existed)
and a stale "type a message" error could survive a valid follow-up
submission. 1ab342 -
fix(content-builder): drop a stale AI response after a page switch (#64) 371c91
-
fix(content-builder): keep the generated document in follow-up history, guard double-submit a48469
-
feat(content-builder): chat panel in the editor for building and refining a page (#64)
Docks a conversational panel into the Content Builder editor's tree panel
via a mixin on bcb-panel.js. Every reply is applied straight into the
working copy through content-builder.js's applyAiComponents(), and every
request is sent persist:false so nothing reaches the draft until the
admin uses the editor's own Save/Publish. 9e66ae -
feat(content-builder): add non-persisting build endpoint and editor chat config (#64)
Adds options.persist=false to Controller/Adminhtml/Page/Build so a request
can return generated components without writing the Content Builder draft.
Extracts vision-model selection out of BuildFromScreenshot into a shared
VisionModelProvider, and adds Block/Adminhtml/Editor/AiChatConfig plus its
layout/template to seed the editor route with the same config. 6eca5e -
Merge pull request #80 from breezefront/claude/focused-payne-b60751
fix(content-builder): reproduce every band the screenshot shows (#74) a84559 -
fix(content-builder): word the band warnings for the request that was sent (#74)
Four defects found reviewing the review fixes.
The count-mismatch warnings hardcoded "The reference design was read as ...",
but the mismatch branch runs whether or not a screenshot was attached — the
prompt asks for a band count on every build, not only on the image path. A
text-only brief that came back over- or under-segmented therefore told the
merchant about a reference design they never sent, on the build note and in
the Content Builder editor. The flag that decides whether to warn now decides
what the warning calls the thing the page was counted against.
The band-wrapper warnings borrowed ComponentValidator's "root[N]" label while
numbering a different list. The validator indexes the raw model output, where
a dropped node still consumes its index; the audit sees the list after
validation. One dropped band was enough to put two warnings saying "root[0]"
in one list, pointing at two different bands. They are numbered as bands now.
readScreenshot() read and base64-encoded the file before ImageValidator got to
apply MAX_IMAGE_BYTES, so an oversized design export cost about 2.3x its size
in memory to reach a verdict available from filesize(). The admin path is
unaffected: the browser posts base64 and no file is read.
FiveBandMock::drawProductRow() took $bottom and never used it, so that band
alone would not follow a change to the band split in draw() — in the one file
whose docblock claims the band boundaries are readable in source. 685502 -
fix(content-builder): keep the band count when the reply carries prose (#74)
Three defects found reviewing the band-audit change.
The prompt says "no prose" and models add it anyway, which is why
jsonCandidates() exists at all. It tries the widest "[...]" run before
"{...}", correct while a bare list was the asked-for shape. With an envelope
whose "unsupported" is omitted and "components" last, that run is exactly the
components array: it parses, wins, and the band count is thrown away. The
audit then reported "the AI did not report how many bands it saw" for a reply
that did report it. A list is now held as a fallback and the remaining
candidates are still tried for an envelope, so a reply that really is a bare
list still resolves to the first list.
spreadStackedSlots() decided a band was stacked from the count of the first
slot alone. "children" written as a flat list of nodes rather than a list of
slots lands there with the node itself as the first slot, and its own keys are
counted as sibling blocks — {"type": ..., "props": ...} counts 2 in a
two-column band and was shredded across the slots, replacing the accurate
short-slot warning with a repair that never happened. Every entry now has to
look like a node.
render.php ignored the file_put_contents result and printed "(0 bytes)" with
status 0 on an unwritable path, sending whoever reproduces a measurement to
look at the build command instead. ee671e -
fix(content-builder): reproduce every band the screenshot shows (#74)
Shown a five-band reference design, gpt-4o-mini returned a document whose
columns bands were fakes: the container had the right type and the right
column count, but every card sat in its first slot, so the band rendered as
one column with the rest empty. Measured over ten runs of the committed mock,
that happened in nine of them.
Three changes, in order of how much they hold:
- ComponentValidator spreads a container whose whole band was written into
its first slot, when the arithmetic leaves no other reading. Prompting
against this only half worked; the repair is unambiguous.
- The prompt asks for {"bands", "unsupported", "components"} instead of a
bare array, spells out the slot shape, and names the columns-versus-
product_grid choice. A bare array is still accepted.
- BandAudit compares the band count the model reports to what it emitted and
reports the difference, plus any band the catalog cannot express. It
reports and does not retry: a second round trip would double the cost of
every build to re-roll an answer a small model is as likely to get wrong
again, and the missing band cannot be synthesised locally.
Measured with the mock and expected shape now under Test/Fixture, driven
through the new --screenshot option. Ten runs each, gpt-4o-mini:
before: 5 bands x5, 6 bands x4, 1 hard failure; 9/9 mis-slotted
after: 5 bands x10, 0 mis-slotted, 8/10 with both columns containers
gpt-4o gets the slots right with either prompt, so the mis-slotting was
model capacity and the prompt closes the gap on the small model. 0e5f35 -
Merge pull request #79 from breezefront/fix/build-note-page-id-type
fix(content-builder): match cms_page.page_id column type in the build note table (#72) 7faf89 -
fix(content-builder): match cms_page.page_id column type in the build note table (#72) 7f6aaa
-
Merge pull request #78 from breezefront/claude/priceless-turing-a344ac
fix(config): offer model names the providers still serve (#73) 21eee6 -
Merge pull request #77 from breezefront/claude/jovial-chatelet-3447d7
feat(content-builder): report what the validator repaired (#72) d23f45 -
fix(content-builder): drop a corrupt build note instead of reporting a clean build (#72) e3994a
-
feat(content-builder): report what the validator repaired (#72) 4b47e4
-
fix(gemini): redact api key from error, notice for retired models (#73) 922d51
-
fix(config): offer model names the providers still serve (#73) b1963c
-
Merge pull request #76 from breezefront/claude/lucid-kepler-a7ce73
fix(content-builder): keep authoring placeholders out of generated copy (#71) 6a5b87 -
fix(content-builder): keep required sample copy on the malformed-value path (#71)
fallbackValue() blanked any authoring placeholder, so a required copy prop the
model sent as an array — quote.text, in the fixture — came out empty, which is
the broken component the omitted-prop path deliberately avoids. The rule now
lives in one predicate used by both paths.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 8a6461 -
fix(content-builder): keep authoring placeholders out of generated copy (#71)
The catalog's "default" attribute does two jobs: the value a renderer needs
when a prop is unset, and the sample text the editor drops into a fresh
component. The validator filled both, so "Discover Something New" shipped as
the heading of three FAQ answers. Copy-typed props whose default reads as
prose are now stored blank — blank, not absent, because the templates read
`$props['heading'] ?? '<sample>'` — and the prompt marks them VISIBLE-TEXT
instead of quoting the sample text that taught the model to echo it.
A list-valued prop is blanked as an empty list, so an unusable faq_items no
longer falls back to the two sample questions. Tabs keeps its declared titles:
the container counts its slots from them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 00e837 -
Merge pull request #75 from breezefront/claude/amazing-lichterman-a86181
fix(content-builder): keep the list props the model wrote (#70) 819b38 -
fix(content-builder): keep every tab title paired with its slot (#70)
Dropping an unusable title compacted the list while normalizeChildren()
truncates slots from the end, so ["Shipping", "", "Returns"] over three
populated slots rendered "Returns" above the middle slot's content and threw
the last one away.
A list of strings is positional — its container pairs slot N with entry N —
so an unusable entry is now replaced where it stands, taking the declared
default's title for that position ("Tab 2"), which is what the container's
renderer pads a short title list with anyway. Entries that carry their own
content, like the FAQ items, have no slot to stay paired with and are still
dropped.
An empty list now says so rather than reporting that none of its zero
entries was usable. efb774 -
fix(content-builder): read a keyed object as one list entry (#70)
decodeList() accepted any array, so a keyed object counted as a list: an FAQ
item the model sent unwrapped — {"q": ..., "a": ...} instead of [{...}] —
was iterated field by field, turning the question and the answer into two
bogus items, each with the other's text as its question.
Only a real list is a list now. A keyed object carrying at least one declared
field is wrapped as the single entry it is, so the question the model wrote
survives; one carrying none falls back to the default with a warning naming
the fields that were expected. 71d179 -
fix(content-builder): keep the list props the model wrote (#70)
The catalog asks the model for the Tabs titles and the FAQ items as JSON
arrays, then ComponentValidator coerced every non-scalar prop back to its
default — so a generated FAQ arrived with its questions replaced by "Tab 1"
/ "Tab 2" / "Tab 3", and the Tabs slot count fell back to the default three,
dropping any further answer with it.
A prop whose declared default is a JSON array is now validated as a list
instead of discarded: entries are cleaned one by one and written back in the
JSON-string shape the editor stores and the renderers decode, so the slot
count follows the titles the model actually wrote. The default is only
reached when nothing usable is left, and the warning then names the reason.
Recognised by the shape of the declared default rather than by prop name, so
the FAQ accordion's question/answer items are covered by the same path. a4765b -
Merge pull request #69 from breezefront/claude/stoic-yonath-58797a
fix(content-builder): drop fake "spacing" prop from catalog prompt 7db871 -
fix(content-builder): drop fake "spacing" prop from catalog prompt
The margin/padding summary line started with the group label instead of
prop names, so the model read "spacing" as a prop and emitted it on every
component that had one. Render one line per value range, leading with the
real names, which also stops claiming -200 as the minimum for paddings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> beb335 -
chore: drop stray .b64 scratch files from screenshots dir
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> e5e228 -
docs: add screenshots to test guide, correct scenarios after live testing
Vision-model dropdown pre-filters server-side (10.6 unreachable via UI as
written), Remove+re-attach confirmed clean (10.9). Found and filed a real
draft-persistence bug in module-breeze-content-builder (#31) — re-run builds
report success but don't survive a fresh page reload.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> 8062ec -
docs: add test scenarios and how-to-test guide for Content Builder screenshot import
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> 8e6e8d -
Merge pull request #67 from breezefront/feat/value-backup
feat(backup): record values before AI writes 70f7cc -
fix(backup): skip ambiguous staging rows 27e298
-
fix(backup): guard capture reads, batch pruning 19f666
-
feat(backup): record values before AI writes 21d8f6
-
Merge pull request #66 from breezefront/test/bulk-selection-regression
test(bulk): pin the scope a run resolves to b69352 -
test(bulk): seed the products the scope cases run against
Copilot was right on both counts: the cases borrowed whatever the
catalogue happened to hold, and the exclusion case skipped itself whenever
no two SKUs shared a five-character prefix. A regression check that goes
quiet when the surrounding data does not suit it is not a guard.
Three products with a prefix unique to the run are created in setUp and
removed in tearDown, so every case is deterministic and none can skip. The
prefix also gets its own case, since the two filtered cases rest on it
selecting this test's products and nothing else.
Reverting BulkEntityIdResolver to the pre-33d5e97 behaviour now turns five
of the six red, up from four. The unfiltered select-all stays green, which
is correct - that path is the legitimate one and behaved the same before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> e82c74 -
test(bulk): pin the scope a run resolves to
The selection fault has landed three times. 15c1e7c introduced an
all_selected flag driven by allSelected(), #31 closed it in June, and
c13907e swapped in excludeMode() and brought it straight back - Magento
raises either flag by itself once the checked rows cover the rows the grid
is showing. Both attempts only ever argued about the client-side flag,
while the resolver went on loading the collection with no filters at all.
The E2E spec covers the request the wizard posts. Nothing covered what
that request then resolves to, which is the number that decides how many
products get rewritten. These cases do, against the live catalogue: an
explicit selection, a genuine select-all, a select-all narrowed by a
filter, exclusions inside a filtered set, and an empty selection.
Reverting BulkEntityIdResolver to the pre-33d5e97 behaviour turns four of
the five red, the exception being the unfiltered select-all - which is
correct, since that case is the legitimate path and behaved the same
before.
Refs #61
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 8e8b71
-
-
1.2.0
-
Version 1.2.0 634a46
-
Merge pull request #63 from breezefront/feat/build-page-from-screenshot
Build a Content Builder page from a screenshot 680ede -
fix(vision): close the second round of review points on #63
The one that mattered: `options['brief']` skipped the fix from the previous
commit entirely. It leaves the history untouched by design, so an
alternating conversation that ended on a user turn got our prompt appended
after it — the exact pair Anthropic answers 400 to, reachable from REST
where the caller supplies both a brief and a history. The prompt is now
folded into a trailing user turn instead of following it, which keeps that
turn's words rather than dropping them. The class docblock claimed "a
history that already alternates keeps alternating"; it does now.
`testSendsTheHistoryFollowedByTheGeneratedPrompt` was pinning the broken
shape — two user turns, asserted as correct — so its history now ends on
an assistant turn and the folding case has tests of its own.
The block hid itself for every reason a build could fail except the two
the controller actually enforces. A role holding `Magento_Cms::page` but
not `Swissup_BreezeAi::index` or `Magento_Cms::save` saw a working-looking
button whose every click came back as the login page, which the modal can
only report as an expired session.
Also from the review: the modal kept the previous brief when reopened, so
it would have ridden the next screenshot unseen; an in-flight FileReader
could re-arm a screenshot the admin had already removed, now invalidated
by a token that `clearScreenshot()` moves on; the drop zone gained
`role="button"` and the brief and model controls gained real labels, since
a placeholder is not one and the select had no accessible name at all.
A warning-free build no longer redirects on its own. The draft is saved by
then but the CMS form behind the modal is not — this request never touched
it — so a title or URL key typed before opening the modal was silently
lost. The editor is offered as a link in both outcomes.
635 unit, 18 integration, 30 e2e green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 3072d9 -
fix(vision): answer the three points Copilot raised on #63
The docs described a cap the code stopped using. `BuildFromScreenshot`
reads `post_max_size`, and its own comment explains why `getMaxFileSize()`
is the wrong ceiling for a base64 string in a text field — the doc still
named `getMaxFileSize()`, which is exactly the sort of mismatch someone
debugging a 413 would be misled by.
The drop zone carries `tabindex="0"` but only answered to a click, so it
could be tabbed to and not opened. Enter and Space now trigger the picker,
with the same guard the click handler needs: the remove control sits
inside the zone and answers to both keys, and swallowing them there would
leave it dead to the keyboard.
`getVisionModels()` is asked for four times while one CMS page form
renders — `canShow`, `getConfigJson`, and `getSelectedModelId` from inside
it — and each ask loads every model to read its metadata. Memoized for the
life of the block, which is one request.
631 tests green; 29 e2e, the new keyboard case among them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 2e6454 -
test(e2e): assert the selection contract the wizard actually posts
33d5e97 replaced the all_selected flag with Magento's own mass-action
contract — one of selected or excluded, plus the grid namespace and the
filters that were on screen — and updated SaveTest, but the Playwright spec
kept asserting the flag that no longer exists. Red on master since then.
The assertion now names what the fix was for: the filters travel with the
selection, which is what stops an exclude-mode Select All from widening past
the rows the grid was showing. f34845 -
test(e2e): cover the build-from-screenshot modal
The one surface unit tests cannot reach. It exists because of a bug that got
all the way to review: the file input is a child of the drop zone, so the
synthetic click jQuery sends it bubbles back into the zone's own handler and
recurses until the stack blows — the picker never opens and only drag and drop
works. "clicking the drop zone opens the file picker" is the guard for that.
Also covers the pre-flight refusal of a non-image, that the refusal sends no
request, the preview and remove cycle, and that a reopened modal carries none
of the previous run's state.
Stops short of pressing Build draft: that would spend a real provider call.
Skips itself where the control legitimately hides — no Content Builder, or no
configured model that reads images. 10934b -
test(mcp): list build_page among the tools the server exposes
McpTest asserts a hardcoded tool list written when the MCP endpoint shipped
with four tools. build_page joined the skill pool in 1bf01f6, and every skill
in the pool is exposed as a tool, so the integration suite has been red since
then. Nothing about the server changed — only what the test expects. f8588c -
feat(vision): build a Content Builder page from a screenshot
PR #55 landed the whole image path — MessageInterface::getImages(),
ImageBuilder, ImageValidator, VisionSupport, per-provider encoding — and
nothing used it. Every caller of build_page passed an empty history, and no
UI produced an images list. This gives the admin a way in.
A button in the Content Builder fieldset of the CMS page form opens a modal:
attach a screenshot, optionally type a brief, pick a vision-capable model.
The file is read with FileReader and posted as a data URI inside
history[0].images — nothing is written to disk. Controller/Adminhtml/Page/Build
runs the skill, saves the result through DraftWriter, and answers with the URL
of the editor the admin reviews it in.
Synchronous rather than an option on the bulk wizard: a reference screenshot
is a per-page input, and one screenshot over N pages produces N near-identical
layouts. GenerateService, ImageBuilder, ImageValidator, AiRequest, Message and
all three providers are used exactly as PR #55 built them, unchanged.
BuildPage now lifts images off the user turns they arrived on and puts them on
the prompt turn it builds, consuming the last user turn as the brief. That
fixes an existing fault: Model\Provider\Claude passes the message list through
unmerged and Anthropic refuses two user turns in a row, so the old
array_merge($history, [prompt]) answered 400 whenever the lastUserMessage()
fallback was the thing being used.
PromptBuilder gains a conditional Reference design block. Two of its five
rules carry the weight: without the band-per-row line the model answers with a
flat component list and the row-wrapping rule loses to the picture, and
without the ignore-brand-and-colours line it copies the screenshot's hex
values into every colour prop and the draft clashes with the merchant's theme.
Verified end to end against gpt-4o-mini on a real storefront screenshot:
six row-wrapped bands, no validator repairs, draft readable in the editor.
With a brief added, the structure holds and the copy follows the brief rather
than transcribing the screenshot.
540 -> 621 unit tests, all green. 9d8854 -
test(mcp): list build_page among the tools the server exposes
McpTest asserts a hardcoded tool list written when the MCP endpoint shipped
with four tools. build_page joined the skill pool in 1bf01f6, and every skill
in the pool is exposed as a tool, so the integration suite has been red since
then. Nothing about the server changed — only what the test expects. 60d8c7 -
feat(vision): build a Content Builder page from a screenshot
PR #55 landed the whole image path — MessageInterface::getImages(),
ImageBuilder, ImageValidator, VisionSupport, per-provider encoding — and
nothing used it. Every caller of build_page passed an empty history, and no
UI produced an images list. This gives the admin a way in.
A button in the Content Builder fieldset of the CMS page form opens a modal:
attach a screenshot, optionally type a brief, pick a vision-capable model.
The file is read with FileReader and posted as a data URI inside
history[0].images — nothing is written to disk. Controller/Adminhtml/Page/Build
runs the skill, saves the result through DraftWriter, and answers with the URL
of the editor the admin reviews it in.
Synchronous rather than an option on the bulk wizard: a reference screenshot
is a per-page input, and one screenshot over N pages produces N near-identical
layouts. GenerateService, ImageBuilder, ImageValidator, AiRequest, Message and
all three providers are used exactly as PR #55 built them, unchanged.
BuildPage now lifts images off the user turns they arrived on and puts them on
the prompt turn it builds, consuming the last user turn as the brief. That
fixes an existing fault: Model\Provider\Claude passes the message list through
unmerged and Anthropic refuses two user turns in a row, so the old
array_merge($history, [prompt]) answered 400 whenever the lastUserMessage()
fallback was the thing being used.
PromptBuilder gains a conditional Reference design block. Two of its five
rules carry the weight: without the band-per-row line the model answers with a
flat component list and the row-wrapping rule loses to the picture, and
without the ignore-brand-and-colours line it copies the screenshot's hex
values into every colour prop and the draft clashes with the merchant's theme.
Verified end to end against gpt-4o-mini on a real storefront screenshot:
six row-wrapped bands, no validator repairs, draft readable in the editor.
With a brief added, the structure holds and the copy follows the brief rather
than transcribing the screenshot.
540 -> 621 unit tests, all green. c53c71
-
-
head
-
Merge pull request #35 from breezefront/feat/ai-components-bridge
feat(content-builder): apply AI-generated components to the working copy (#34) fa5587 -
Added new Video component 94bb8d
-
fix(content-builder): clone the incoming array before installing it as the working copy d02528
-
fix(content-builder): stay a no-op until sub-modules finish initializing 2c2289
-
fix(content-builder): commit a pending dirty edit before pushing the AI snapshot 3a1ce7
-
feat(content-builder): apply AI-generated components to the working copy (#34) 030d4c
-
Added 5 columns layout for the Columns component 4adc04
-
-
1.0.12
-
Version 1.0.12 ccc005
-
fix(banner): make the duplicate-H1 guard see the theme's own H1 (#32)
Three review findings on the H1 work:
1. The guard counted only Banner components, so it stayed silent on the most
common duplicate: a CMS page with a Content Heading, where
Magento\Cms\Block\Page::_prepareLayout() feeds page.main.title and
module-theme's html/title.phtml emits <h1 class="page-title">. Load now
reports pageHasH1 (Content Heading non-blank), setExternalContent takes the
same flag from its opts so BLB can declare the product/category page title,
and the guard adds that H1 to the count.
2. The guard only ran on a headline_type change, so pasting an H1 Banner never
warned. It now runs after paste and after content load as well, which also
covers a tree that arrived from an AI build.
3. getDraft() reloaded the draft by id without checking the load succeeded. If
the row was deleted between the collection query and the reload, it returned
an empty model and the caller saved a row with no page_id — rejected by
MySQL, since page_id is NOT NULL with no default. It now falls back to the
seeded-draft path, which is extracted into createSeededDraft().
Load's constructor gained a dependency, so setup:di:compile is needed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> abc266 -
feat(banner): allow H1 for the banner headline (#32)
The Banner headline_type prop only offered h2-h6, so a page built entirely
in Content Builder shipped without a primary heading — the document had no
h1 at all, which is an on-page SEO defect.
Add h1 to the option list and to the template's tag whitelist. The default
stays h2, so existing pages and newly inserted Banners render exactly as
before.
Because a page should carry at most one h1, the editor now shows a soft
notice when a headline is switched to h1 while another Banner already uses
it. It warns rather than blocks — a legitimate layout may keep its h1
outside the Content Builder bands.
The Text component's heading_type is deliberately left alone: it renders a
div with a heading class, not a real heading tag, so an "h1" there would be
visual only and misleading.
Fixes #32
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> ced844 -
fix(content): reload draft through singular load so updated_at advances
getDraft() returned a collection-hydrated item with empty storedData,
so save()'s update-diff wrote every column back unchanged, pinning
updated_at and defeating ON UPDATE CURRENT_TIMESTAMP.
Related to #31 (does not close it — content-loss symptom unconfirmed). b1759b -
fix(banner): make the duplicate-H1 guard see the theme's own H1 (#32)
Three review findings on the H1 work:
1. The guard counted only Banner components, so it stayed silent on the most
common duplicate: a CMS page with a Content Heading, where
Magento\Cms\Block\Page::_prepareLayout() feeds page.main.title and
module-theme's html/title.phtml emits <h1 class="page-title">. Load now
reports pageHasH1 (Content Heading non-blank), setExternalContent takes the
same flag from its opts so BLB can declare the product/category page title,
and the guard adds that H1 to the count.
2. The guard only ran on a headline_type change, so pasting an H1 Banner never
warned. It now runs after paste and after content load as well, which also
covers a tree that arrived from an AI build.
3. getDraft() reloaded the draft by id without checking the load succeeded. If
the row was deleted between the collection query and the reload, it returned
an empty model and the caller saved a row with no page_id — rejected by
MySQL, since page_id is NOT NULL with no default. It now falls back to the
seeded-draft path, which is extracted into createSeededDraft().
Load's constructor gained a dependency, so setup:di:compile is needed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> a6d73f -
feat(banner): allow H1 for the banner headline (#32)
The Banner headline_type prop only offered h2-h6, so a page built entirely
in Content Builder shipped without a primary heading — the document had no
h1 at all, which is an on-page SEO defect.
Add h1 to the option list and to the template's tag whitelist. The default
stays h2, so existing pages and newly inserted Banners render exactly as
before.
Because a page should carry at most one h1, the editor now shows a soft
notice when a headline is switched to h1 while another Banner already uses
it. It warns rather than blocks — a legitimate layout may keep its h1
outside the Content Builder bands.
The Text component's heading_type is deliberately left alone: it renders a
div with a heading class, not a real heading tag, so an "h1" there would be
visual only and misleading.
Fixes #32
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 620e7f -
fix(content): reload draft through singular load so updated_at advances
getDraft() returned a collection-hydrated item with empty storedData,
so save()'s update-diff wrote every column back unchanged, pinning
updated_at and defeating ON UPDATE CURRENT_TIMESTAMP.
Related to #31 (does not close it — content-loss symptom unconfirmed). 5c797e -
Remove custom accordion script d8cb73
-
Added new Accordion/FAQ component 3135bd
-
-
head
-
Update composer.json 1afacb
-
-
head
-
Check theme source files in Virtual Theme Check (#19)
Making a virtual theme physical while its source files are unavailable is
what breaks the storefront with "Required parameter 'theme_dir' was not
passed", so the check now reports the state of the files and the fixer
refuses to touch the themes it cannot read.
- Virtual Theme Check gets Path and Status columns
- Status names the files Magento can't read. Unreadable file is never
reported as a removed one - a failed stat cannot tell them apart
- Fix All opens up the permissions of unreadable source files and skips
the themes that are still unreadable, listing them in the response
- Readability check extracted into Swissup\Core\Model\Theme\SourceFiles,
shared by the block and the controller
- Config table styles shared between Modules and Virtual Theme Check
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 88915a -
Check theme source files in Virtual Theme Check (#19)
Making a virtual theme physical while its source files are unavailable is
what breaks the storefront with "Required parameter 'theme_dir' was not
passed", so the check now reports the state of the files and the fixer
refuses to touch the themes it cannot read.
- Virtual Theme Check gets Path and Status columns
- Status names the files Magento can't read. Unreadable file is never
reported as a removed one - a failed stat cannot tell them apart
- Fix All opens up the permissions of unreadable source files and skips
the themes that are still unreadable, listing them in the response
- Readability check extracted into Swissup\Core\Model\Theme\SourceFiles,
shared by the block and the controller
- Config table styles shared between Modules and Virtual Theme Check
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 6e39e2 -
Mark core section with badge when outdated modules found 9a3764
-
Outdated modules badge and Modules config group (#25) 7df026
-
MCS fix 3b9121
-
Update instructions 0e686f
-
Read links info from marketplace meta 21e95b
-
Keep the last check time when a refresh fails
Removing the stored time was what made the next load re-check the feed.
An unreachable feed left no time at all: the config page had nothing to
display, and every page load kept re-checking until one request made it
through.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 3437f9 -
Post the modules refresh to an action of its own
The refresh drops the version check throttle and re-reads the remote
source, and a get carries no form key validation - Magento checks it for
post requests only, and the url secret key is optional. Reading the list
stays a get, so the two live in separate actions now.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 4a95c9 -
Keep the module links out of the response
The links cell is rendered by the server and never touched by js.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 7556db -
Send only the module metadata the browser renders
The merged Loader record carries the local filesystem path and the
download, license and purchase-code fields of the remote feed. None of
them is rendered by the config page, so keep them on the server.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 2bd425 -
Too much red 2e307e
-
Use the same `outdated` class name everywhere
The version span was already renamed, while the row and the js still said
`_outdated` - a row that stopped being outdated kept its red version pill.
Since the row now wears the very same class, the pill rule had to be
scoped to the version cell, or the whole row would turn red.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> c21086 -
Report the check time with the modules, and keep outdated rows first
Two things the clients were guessing at:
The `Check for Updates` handler stamped the label with the current time,
but the request may have re-used the stored packages after a failed feed
request - `just now` was not always true. And an automatic load did check
the remote source, yet nothing updated the label at all. The endpoint now
returns `['items' => ..., 'last_check' => ...]`, and the label follows the
time the server reports for every load.
The table was re-rendered cell by cell, so a module that became outdated
kept its old position while the counter above it grew. Mark the rows and
move the outdated ones back to the top - both groups stay sorted by name.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 70f271 -
Keep the last check time once the check is due
The timestamp was stored with a one hour lifetime and the throttle was
`is the entry still there?`. Handy, but it also meant the config page
could no longer tell how old the data was as soon as the check was due -
exactly when that is worth showing next to `Check for Updates`.
Store it without a lifetime and compare the age instead. Same hour
boundary, and the label now reads `Last checked 2 h ago` instead of
disappearing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 9e4c2a -
Don't update the modules on refresh. Wait for isVersionCheckRequired 129e66
-
Hovered color for outdated badge 0a0016
-
Show outdated modules count in config and add Modules group
The admin had no hint that installed Swissup modules have updates
available - one had to open Module Manager to find out.
Add a counter badge to the Swissup tab of the config navigation, and a
Modules group in Swissup > Core listing every installed module with its
installed and latest version, outdated ones first.
Neither of them may slow down the config page, so both render from the
data stored in var/swissup/core (Loader::setOfflineMode()), and the
versions, the counters and the badge itself are then refreshed by a
single ajax request shared by all the consumers. `Check for Updates`
re-runs that request with refresh=1, which drops the version check
throttle and re-reads the remote source.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> bf1ad6 -
Move core config to the top as it will display outdated modules 115f37
-
Added ability to immidiately re-check remote source for updates `-r` 357d33
-
Keep the remote packages data in files instead of the cache (#24) cbeecc
-
Trim the comments down to what is not in the code
The two lock docblocks carried the same paragraph, and save() explained
the reasoning that belongs in the commit that introduced it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 09d6b9 -
Serialize the writes with a lock taken before the entry is opened
openFile() truncates whatever it opens, so a lock taken afterwards keeps
the writes apart but not the truncations. Two saves could interleave into
one file: the second one truncates, the first one writes a whole entry,
and the second one then overwrites its beginning - leaving the head of
one entry on the tail of another. The length line does not catch that
when both entries encode to the same size, as fixed width values like a
version hash always do.
Take the lock before opening, on a file of its own - the entry cannot be
opened before the lock is held, and the file lock() uses is likely held
by the caller already, which flock would deadlock against.
Failing to open a lock file is no longer reported as a lock held by
somebody else. A lock left behind by another user cannot be opened at
all, and reporting that as contention kept the caller away from the
entry for good - silently serving a list that could never be refreshed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 73cf28 -
Don't download the list that another process has just stored
The version is fetched before the download lock is taken, so a process
that was still asking for it when the lock was released would go on to
download the very list the lock holder had just stored. Look at the
storage once more before downloading.
Also correct what the save() lock claims to do: it serializes the
writes, but the truncation happens in openFile(), outside of it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> d557a0 -
Reject an entry whose expiration time is not a number
A corrupt header such as `expires:invalid` was cast to zero, which reads
as "never expires" - so an entry that should have gone stale could be
served indefinitely instead of being treated as missing. Validate the
field the same way the length line is validated.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 12db16 -
Keep the remote packages data in files instead of the cache
The packages list was kept in the Magento cache, so every cache flush
threw it away and the next admin request had to download it again. It is
now stored under var/swissup/core, where it outlives the flush.
FileStorage writes the expiration time and the data length ahead of the
contents, so an entry knows itself when it goes stale, and a reader can
tell a half-written entry from a complete one - openFile() truncates the
file before the write lock is taken, so a reader can catch the entry
mid-save. An incomplete entry is reported as missing and refetched,
rather than served as if it were whole.
Remote revalidates against the version hash from packages.json, at most
once an hour, and only downloads the full list when that hash moves. The
download itself is guarded by a non-blocking lock, so several admins
hitting the page at once produce one request instead of one each - the
others serve the stored copy, or wait for the download when there is
nothing stored yet. fetch() now reports connection errors and 4xx/5xx
responses by returning an empty body, letting the caller fall back to
the stored data instead of decoding a failure page.
The admin notification feed keeps its last-update timestamp in the same
storage, replacing its own copy of the file handling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> 617dbd -
Small fixes f7eef6
-
Single source of truth for is_outdated, is_installed 37ad01
-
Fixed `Undefined array key "type"` warning b2a9c1
-
Don't show non installed modules when asking for outdated bf5fb1
-