fix: manifest-aware layout picker, real carousel items-per-page, hero arrows/swipe/2-panel
Some checks failed
Architecture Governance / architecture (push) Has been cancelled

Sprint E: homepage section editor now filters the layout-strategy picker
to each widget's widget-manifest.json supportedLayouts instead of always
showing all 5 strategies. columns field gated to widgets that read it
(hero, product-collection carousel).

Sprint F: closes client bug report (no items-per-page control, hero
carousel not manually/automatically scrollable, no 1-2 slide big-carousel
option). Product carousel item width now driven by layout.columns
(reused, was already editable but dead). Hero widget gains prev/next
arrows, touch swipe, and 1-2 panel mode via the same field.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
sdarbinyan
2026-08-05 19:25:34 +04:00
parent 3e3185cb6e
commit 55b379bd6d
9 changed files with 217 additions and 20 deletions

View File

@@ -38,21 +38,23 @@
[usedByLabel]="'builder.usedByLabel' | translate"
>
<app-visual-layout-picker
[options]="layoutStrategyPickerOptions"
[options]="layoutOptionsFor(section)"
[ariaLabel]="'builder.layoutStrategyLabel' | translate"
[ngModel]="section.layout?.strategy || 'stack'"
(ngModelChange)="updateLayout(section.id, 'strategy', $event)"
/>
</app-form-field>
<app-form-field
appHighlightSource="spacing"
[label]="'builder.columns' | translate"
[hint]="'builder.sectionColumnsDesc' | translate"
[usedBy]="[blockLabel(section.type)]"
[usedByLabel]="'builder.usedByLabel' | translate"
>
<app-input type="number" [ngModel]="section.layout?.columns || 1" (ngModelChange)="updateLayout(section.id, 'columns', $event)" />
</app-form-field>
@if (showColumnsFor(section)) {
<app-form-field
appHighlightSource="spacing"
[label]="'builder.columns' | translate"
[hint]="'builder.sectionColumnsDesc' | translate"
[usedBy]="[blockLabel(section.type)]"
[usedByLabel]="'builder.usedByLabel' | translate"
>
<app-input type="number" [ngModel]="section.layout?.columns || 1" (ngModelChange)="updateLayout(section.id, 'columns', $event)" />
</app-form-field>
}
</div>
</div>

View File

@@ -1,5 +1,6 @@
import { CdkDragDrop, DragDropModule, moveItemInArray } from '@angular/cdk/drag-drop';
import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { ProjectEditorFacade } from '../facade/project-editor.facade';
import { TranslatePipe } from '../../../i18n/translate.pipe';
@@ -16,6 +17,8 @@ import { ConfirmDialogComponent } from '../../../shared/ui/confirm-dialog/confir
import { FormFieldComponent } from '../../../shared/ui/form-field/form-field.component';
import { VisualLayoutPickerComponent, VisualLayoutOption } from '../../../shared/ui/visual-layout-picker/visual-layout-picker.component';
import { HighlightSourceDirective } from '../../../shared/ui/highlight-source/highlight-source.directive';
import { WidgetManifestService } from '../../../widgets/registry/widget-manifest.service';
import { WidgetLayoutSupport, WidgetManifestEntry } from '../../../widgets/contracts/widget-manifest.contract';
export interface LayoutStrategyOption {
value: 'stack' | 'grid' | 'hero' | 'carousel' | 'split';
@@ -45,6 +48,9 @@ const BLOCK_CATALOG: BlockCatalogEntry[] = [
const BLOCK_BY_TYPE = new Map(BLOCK_CATALOG.map(entry => [entry.type, entry]));
/** Widget componentKeys (widget-manifest.json) that read `section.layout.columns`. Keep in sync with the widget components that actually consume it - see hero-widget.component.ts and product-carousel-widget.component.ts. */
const COMPONENT_KEYS_READING_COLUMNS = new Set(['hero', 'product-collection']);
@Component({
selector: 'app-project-editor-homepage-section',
standalone: true,
@@ -56,7 +62,11 @@ const BLOCK_BY_TYPE = new Map(BLOCK_CATALOG.map(entry => [entry.type, entry]));
export class ProjectEditorHomepageSectionComponent {
private readonly facade = inject(ProjectEditorFacade);
private readonly translate = inject(TranslateService);
private readonly widgetManifest = inject(WidgetManifestService);
readonly homePage = this.facade.homepagePage;
private readonly manifestEntries = toSignal(this.widgetManifest.getWidgets(), { initialValue: [] as WidgetManifestEntry[] });
private readonly manifestByType = computed(() => new Map(this.manifestEntries().map(entry => [entry.type, entry])));
readonly fieldError = (key: string): string | null => {
const messageKey = this.facade.fieldError(key);
return messageKey ? this.translate.t(messageKey) : null;
@@ -89,6 +99,35 @@ export class ProjectEditorHomepageSectionComponent {
icon: this.layoutStrategyIcons[option.value],
}));
/** Layout strategy options filtered to what the section's bound widget actually supports (widget-manifest.json `supportedLayouts`). Keeps a stale/unsupported saved value selectable instead of rendering a picker with no active card. */
layoutOptionsFor(section: SectionConfig): VisualLayoutOption[] {
const supported = this.manifestByType().get(section.type)?.supportedLayouts;
if (!supported || supported.length === 0) {
return this.layoutStrategyPickerOptions;
}
const options = this.layoutStrategyPickerOptions.filter(option => supported.includes(option.value as WidgetLayoutSupport));
const current = section.layout?.strategy;
if (current && !options.some(option => option.value === current)) {
const currentOption = this.layoutStrategyPickerOptions.find(option => option.value === current);
if (currentOption) {
return [...options, currentOption];
}
}
return options;
}
/** Whether the section's bound widget consumes `layout.columns` (see COMPONENT_KEYS_READING_COLUMNS). Product carousel only uses it in carousel strategy - grid mode ignores it. */
showColumnsFor(section: SectionConfig): boolean {
const componentKey = this.manifestByType().get(section.type)?.componentKey;
if (!componentKey || !COMPONENT_KEYS_READING_COLUMNS.has(componentKey)) {
return false;
}
if (componentKey === 'product-collection') {
return (section.layout?.strategy ?? 'stack') === 'carousel';
}
return true;
}
blockLabel(type: string): string {
const entry = BLOCK_BY_TYPE.get(type);
return entry ? this.translate.t(entry.labelKey) : type;