feat(builder): redesign marketplace builder experience and navigation
Renamed the experience consistently to Marketplace Builder everywhere visible (page title/subtitle, breadcrumbs, sidebar, dashboard quick action/shortcut copy) - internal ProjectEditor class/selector names kept as-is to avoid regressions. builder.title/subtitle no longer say 'bootstrap-config editing surface' to end users. New business-oriented IA (builder-groups.model.ts): 12 existing ProjectEditorSectionIds regrouped into 8 groups (Marketplace, Branding and Design, Homepage, Content, Languages, Marketplace Features, Navigation and Search, Preview) - every existing section mapped to exactly one group, none dropped, no group points at a page that doesn't exist. New Builder landing page at /edit (was a redirect straight into the General form): readiness percent, recommended next step, one overview card per group (purpose + real complete/in-progress/not-started/unknown status, icon+text never color-alone), a Marketplace Readiness checklist, quick links. All derived from real facade/schema signals (required-field fill state, modifiedSections, staticPages, catalog counts via BackofficeDataService) or explicitly marked unknown (the "preview reviewed" check has no tracking - shown as unknown, never guessed). Section pages (/edit/:section) now share one consistent header: breadcrumb (Builder > Group > Section), draft/published + modified badges, and a contextual help panel (what is this / where visible / what happens if you skip it) sourced per group. Sidebar nav rewritten as grouped, icon-led sections with per-section and per-group status indicators; layout converted from a top pill-tab bar to a responsive sidebar (desktop 280px, tablet 72px icon rail, mobile drawer with Escape-to-close), matching the Sprint 1 admin shell pattern. Section form components themselves untouched. Routes: 'edit' is now its own landing route instead of redirecting to 'edit/general'; /builder and /project-editor redirect to 'edit'. Sprint 1/2 destinations that pointed at edit/general now point at the new edit landing page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,32 @@
|
||||
<nav class="editor-nav" [attr.aria-label]="'builder.title' | translate">
|
||||
@for (section of sections; track section.id) {
|
||||
<a
|
||||
[routerLink]="['/edit', section.id] | langRoute"
|
||||
routerLinkActive="active"
|
||||
[ariaCurrentWhenActive]="'page'"
|
||||
class="editor-nav-link"
|
||||
>{{ section.label | translate }}@if (issueCount(section.id); as count) {<span class="editor-nav-badge" [attr.aria-label]="count + ' ' + ('builder.title' | translate)">{{ count }}</span>} @else if (isModified(section.id)) {<span class="editor-nav-dot" [attr.aria-label]="'builder.unsavedChanges' | translate"></span>}</a>
|
||||
<nav class="editor-nav" [attr.aria-label]="'builder.appName' | translate">
|
||||
@for (item of groups(); track item.group.id) {
|
||||
<div class="editor-nav-group">
|
||||
<div class="editor-nav-group__header">
|
||||
<span class="pi {{ item.group.icon }} editor-nav-group__icon" aria-hidden="true"></span>
|
||||
<span class="editor-nav-group__label">{{ item.group.labelKey | translate }}</span>
|
||||
<span class="pi {{ item.statusIcon }} editor-nav-group__status editor-nav-group__status--{{ item.status }}" [attr.aria-label]="item.statusLabelKey | translate"></span>
|
||||
</div>
|
||||
<ul class="editor-nav-group__list">
|
||||
@for (section of item.sections; track section.id) {
|
||||
<li>
|
||||
<a
|
||||
[routerLink]="['/edit', section.id] | langRoute"
|
||||
routerLinkActive="active"
|
||||
[ariaCurrentWhenActive]="'page'"
|
||||
class="editor-nav-link"
|
||||
(click)="onLinkClick()"
|
||||
>
|
||||
<span class="pi {{ section.statusIcon }} editor-nav-link__status editor-nav-link__status--{{ section.status }}" [attr.aria-label]="section.statusLabelKey | translate"></span>
|
||||
<span class="editor-nav-link__label">{{ section.labelKey | translate }}</span>
|
||||
@if (section.issueCount > 0) {
|
||||
<span class="editor-nav-badge" [attr.aria-label]="section.issueCount + ' ' + ('builder.title' | translate)">{{ section.issueCount }}</span>
|
||||
} @else if (section.modified) {
|
||||
<span class="editor-nav-dot" [attr.aria-label]="'builder.unsavedChanges' | translate"></span>
|
||||
}
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
</nav>
|
||||
|
||||
@@ -1,79 +1,144 @@
|
||||
.editor-nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm, 10px);
|
||||
flex-direction: column;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.editor-nav-group__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
padding: 0 var(--space-sm);
|
||||
margin-bottom: var(--space-xs);
|
||||
}
|
||||
|
||||
.editor-nav-group__icon {
|
||||
color: var(--primary-color);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.editor-nav-group__label {
|
||||
flex: 1 1 auto;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.4px;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.editor-nav-group__status {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.editor-nav-group__status--complete {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.editor-nav-group__status--in-progress {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.editor-nav-group__status--not-started {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.editor-nav-group__status--unknown {
|
||||
color: var(--text-light);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.editor-nav-group__list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.editor-nav-link {
|
||||
min-height: 40px;
|
||||
border-radius: var(--radius-full, 999px);
|
||||
border: 1px solid var(--border-color, #d3dad9);
|
||||
background: var(--bg-primary, #fff);
|
||||
color: var(--text-primary, #1e3c38);
|
||||
padding: 0 var(--space-md, 14px);
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
min-height: 38px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--text-primary);
|
||||
padding: 0 var(--space-sm);
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
line-height: 38px;
|
||||
transition: background-color var(--transition-fast, 120ms ease),
|
||||
border-color var(--transition-fast, 120ms ease),
|
||||
color var(--transition-fast, 120ms ease);
|
||||
|
||||
&:hover {
|
||||
border-color: var(--primary-color, #2f6e5d);
|
||||
background: color-mix(in srgb, var(--primary-color) 8%, transparent);
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--primary-color, #2f6e5d);
|
||||
outline: 2px solid var(--primary-color);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: var(--primary-color, #2f6e5d);
|
||||
background: var(--primary-color, #2f6e5d);
|
||||
color: var(--bg-primary, #fff);
|
||||
background: color-mix(in srgb, var(--primary-color) 8%, transparent);
|
||||
color: var(--primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-nav-link__status {
|
||||
font-size: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-nav-link__status--complete {
|
||||
color: var(--success-color);
|
||||
}
|
||||
|
||||
.editor-nav-link__status--in-progress {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.editor-nav-link__status--not-started {
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.editor-nav-link__status--unknown {
|
||||
color: var(--text-light);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.editor-nav-link__label {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.editor-nav-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
margin-left: 6px;
|
||||
padding: 0 5px;
|
||||
border-radius: 999px;
|
||||
background: var(--error-color, #ef4444);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--error-color);
|
||||
color: #fff;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.editor-nav-link.active .editor-nav-badge {
|
||||
background: #fff;
|
||||
color: var(--error-color, #ef4444);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-nav-dot {
|
||||
display: inline-block;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-left: 6px;
|
||||
border-radius: 999px;
|
||||
background: var(--warning-color, #f59e0b);
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.editor-nav-link.active .editor-nav-dot {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.editor-nav-link {
|
||||
transition: none;
|
||||
}
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--warning-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,57 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { ChangeDetectionStrategy, Component, computed, inject, input, output } from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive } from '@angular/router';
|
||||
import { TranslatePipe } from '../../../i18n/translate.pipe';
|
||||
import { LangRoutePipe } from '../../../pipes/lang-route.pipe';
|
||||
import { ProjectEditorSectionId } from '../models/project-editor.model';
|
||||
import { ProjectEditorSectionId, BuilderSectionStatus } from '../models/project-editor.model';
|
||||
import { ProjectEditorFacade } from '../facade/project-editor.facade';
|
||||
import { BUILDER_GROUPS, BuilderGroup } from '../builder/builder-groups.model';
|
||||
|
||||
const STATUS_ICON: Record<BuilderSectionStatus, string> = {
|
||||
complete: 'pi-check-circle',
|
||||
'in-progress': 'pi-circle-fill',
|
||||
'not-started': 'pi-circle',
|
||||
unknown: 'pi-question-circle',
|
||||
};
|
||||
|
||||
const STATUS_LABEL_KEY: Record<BuilderSectionStatus, string> = {
|
||||
complete: 'builder.statusComplete',
|
||||
'in-progress': 'builder.statusInProgress',
|
||||
'not-started': 'builder.statusNotStarted',
|
||||
unknown: 'builder.statusUnknown',
|
||||
};
|
||||
|
||||
interface NavSectionItem {
|
||||
id: ProjectEditorSectionId;
|
||||
labelKey: string;
|
||||
status: BuilderSectionStatus;
|
||||
statusIcon: string;
|
||||
statusLabelKey: string;
|
||||
modified: boolean;
|
||||
issueCount: number;
|
||||
}
|
||||
|
||||
interface NavGroupItem {
|
||||
group: BuilderGroup;
|
||||
status: BuilderSectionStatus;
|
||||
statusIcon: string;
|
||||
statusLabelKey: string;
|
||||
sections: NavSectionItem[];
|
||||
}
|
||||
|
||||
const SECTION_LABEL_KEYS: Record<ProjectEditorSectionId, string> = {
|
||||
general: 'builder.general',
|
||||
branding: 'builder.branding',
|
||||
theme: 'builder.theme',
|
||||
header: 'builder.header',
|
||||
footer: 'builder.footer',
|
||||
homepage: 'builder.homepage',
|
||||
widgets: 'builder.widgets',
|
||||
'static-pages': 'builder.staticPages',
|
||||
features: 'builder.marketplaceFeatures',
|
||||
languages: 'builder.languagesTab',
|
||||
navigation: 'builder.navigationTab',
|
||||
preview: 'builder.preview',
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-project-editor-nav',
|
||||
@@ -11,35 +59,40 @@ import { ProjectEditorFacade } from '../facade/project-editor.facade';
|
||||
imports: [TranslatePipe, RouterLink, RouterLinkActive, LangRoutePipe],
|
||||
templateUrl: './project-editor-nav.component.html',
|
||||
styleUrls: ['./project-editor-nav.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ProjectEditorNavComponent {
|
||||
private readonly facade = inject(ProjectEditorFacade);
|
||||
|
||||
readonly linkClicked = output<void>();
|
||||
|
||||
private readonly issuesBySection = this.facade.issuesBySection;
|
||||
private readonly modifiedSections = this.facade.modifiedSections;
|
||||
private readonly sectionStatus = this.facade.sectionStatus;
|
||||
private readonly groupStatus = this.facade.groupStatus;
|
||||
|
||||
/** Count of blocking issues in a section, for the nav badge. */
|
||||
issueCount(sectionId: ProjectEditorSectionId): number {
|
||||
return this.issuesBySection().get(sectionId) ?? 0;
|
||||
readonly groups = computed<NavGroupItem[]>(() =>
|
||||
BUILDER_GROUPS.map(group => ({
|
||||
group,
|
||||
status: this.groupStatus().get(group.id) ?? 'unknown',
|
||||
statusIcon: STATUS_ICON[this.groupStatus().get(group.id) ?? 'unknown'],
|
||||
statusLabelKey: STATUS_LABEL_KEY[this.groupStatus().get(group.id) ?? 'unknown'],
|
||||
sections: group.sections.map(id => {
|
||||
const status = this.sectionStatus().get(id) ?? 'unknown';
|
||||
return {
|
||||
id,
|
||||
labelKey: SECTION_LABEL_KEYS[id],
|
||||
status,
|
||||
statusIcon: STATUS_ICON[status],
|
||||
statusLabelKey: STATUS_LABEL_KEY[status],
|
||||
modified: this.modifiedSections().has(id),
|
||||
issueCount: this.issuesBySection().get(id) ?? 0,
|
||||
};
|
||||
}),
|
||||
})),
|
||||
);
|
||||
|
||||
onLinkClick(): void {
|
||||
this.linkClicked.emit();
|
||||
}
|
||||
|
||||
/** Whether a section has unsaved modified fields, for the nav dot. */
|
||||
isModified(sectionId: ProjectEditorSectionId): boolean {
|
||||
return this.modifiedSections().has(sectionId);
|
||||
}
|
||||
|
||||
readonly sections: Array<{ id: ProjectEditorSectionId; label: string }> = [
|
||||
{ id: 'general', label: 'builder.general' },
|
||||
{ id: 'branding', label: 'builder.branding' },
|
||||
{ id: 'theme', label: 'builder.theme' },
|
||||
{ id: 'header', label: 'builder.header' },
|
||||
{ id: 'footer', label: 'builder.footer' },
|
||||
{ id: 'homepage', label: 'builder.homepage' },
|
||||
{ id: 'widgets', label: 'builder.widgets' },
|
||||
{ id: 'static-pages', label: 'builder.staticPages' },
|
||||
{ id: 'features', label: 'builder.marketplaceFeatures' },
|
||||
{ id: 'languages', label: 'builder.languagesTab' },
|
||||
{ id: 'navigation', label: 'builder.navigationTab' },
|
||||
{ id: 'preview', label: 'builder.preview' },
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user