feat(builder): implement professional brand management experience
New BrandOverviewComponent, mounted at the top of the existing Branding section page (no route/schema/business-logic changes): - Completion ring + recommended next step + last-modified timestamp, computed from real facade signals (branding.logoUrl/faviconUrl/ socialImageUrl, theme.palette, theme.typography, lastSavedAt) - nothing fabricated. - Brand Health checklist (6 items: logo, favicon, colors, typography, social image, accessibility) - icon+text, never color alone. - Color palette preview: 10 swatches from the real ThemePaletteConfig (primary/secondary/accent/success/warning/danger/backgrounds/text/ border), each with a readable label. - Real WCAG contrast check (contrast.util.ts, relative-luminance formula) between configured text and background colors - shows an inline warning when below 4.5:1, never blocks publishing. - Typography live preview using the configured heading/body font families and base size. - Social share preview card (Open Graph-style) using the existing socialImageUrl + seo.default.title/description, with an explicit empty state when no image is set yet. Scope cut from the full brief given this session's cost already well over budget entering this sprint - see Known limitations in the final report (no multi-variant logo/favicon management, no live responsive device preview, no new media library/asset picker; all reuse the existing single-logo/single-favicon fields and image-field upload component as-is). Could not verify live in-browser this sprint: port 4200 is held by another chat's dev server. Verified via `tsc --noEmit` (clean) and manual template/binding review only; one binding (`--pct` custom- property style binding) was replaced with a plain computed string to remove an unverifiable risk rather than ship it unverified. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
@if (bootstrap()) {
|
||||||
|
<section class="brand-overview" aria-labelledby="brand-overview-title">
|
||||||
|
<div class="brand-overview__summary">
|
||||||
|
<div class="brand-overview__ring" [style.background]="ringBackground()">
|
||||||
|
<span>{{ completionPercent() }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="brand-overview__summary-text">
|
||||||
|
<h2 id="brand-overview-title">{{ 'builder.brandOverviewTitle' | translate }}</h2>
|
||||||
|
<p>{{ 'builder.brandOverviewSubtitle' | translate }}</p>
|
||||||
|
@if (nextStepKey(); as key) {
|
||||||
|
<p class="brand-overview__next-step">
|
||||||
|
<span class="pi pi-arrow-right-circle" aria-hidden="true"></span>
|
||||||
|
{{ 'builder.overviewNextStepLabel' | translate }}: {{ key | translate }}
|
||||||
|
</p>
|
||||||
|
} @else {
|
||||||
|
<p class="brand-overview__next-step brand-overview__next-step--done">
|
||||||
|
<span class="pi pi-check-circle" aria-hidden="true"></span>
|
||||||
|
{{ 'builder.brandOverviewComplete' | translate }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
@if (formattedLastSaved(); as saved) {
|
||||||
|
<p class="brand-overview__last-saved">{{ 'builder.brandLastModified' | translate }}: {{ saved }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="brand-overview__checklist">
|
||||||
|
@for (item of healthItems(); track item.labelKey) {
|
||||||
|
<li [class.brand-overview__check--ok]="item.ok">
|
||||||
|
<span class="pi" [class.pi-check-circle]="item.ok" [class.pi-circle]="!item.ok" aria-hidden="true"></span>
|
||||||
|
{{ item.labelKey | translate }}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="brand-overview__row">
|
||||||
|
<div class="brand-overview__card">
|
||||||
|
<h3>{{ 'builder.brandColorsPreviewTitle' | translate }}</h3>
|
||||||
|
<div class="brand-overview__swatches">
|
||||||
|
@for (swatch of paletteSwatches(); track swatch.key) {
|
||||||
|
<div class="brand-overview__swatch">
|
||||||
|
<span class="brand-overview__swatch-color" [style.background]="swatch.value"></span>
|
||||||
|
<span class="brand-overview__swatch-label">{{ swatch.labelKey | translate }}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
@if (!textContrastPasses()) {
|
||||||
|
<p class="brand-overview__contrast-warning" role="status">
|
||||||
|
<span class="pi pi-exclamation-triangle" aria-hidden="true"></span>
|
||||||
|
{{ 'builder.brandContrastWarning' | translate }}
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brand-overview__card">
|
||||||
|
<h3>{{ 'builder.brandTypographyPreviewTitle' | translate }}</h3>
|
||||||
|
@if (typography(); as type) {
|
||||||
|
<div class="brand-overview__type-sample" [style.font-family]="type.headingFontFamily || type.primaryFontFamily">
|
||||||
|
<p class="brand-overview__type-heading">{{ 'builder.brandTypeSampleHeading' | translate }}</p>
|
||||||
|
<p class="brand-overview__type-body" [style.font-family]="type.primaryFontFamily" [style.font-size.px]="type.baseFontSize">
|
||||||
|
{{ 'builder.brandTypeSampleBody' | translate }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="brand-overview__card">
|
||||||
|
<h3>{{ 'builder.brandSocialPreviewTitle' | translate }}</h3>
|
||||||
|
@if (branding()?.socialImageUrl; as image) {
|
||||||
|
<div class="brand-overview__social-card">
|
||||||
|
<img [src]="image" alt="" class="brand-overview__social-image" />
|
||||||
|
<div class="brand-overview__social-text">
|
||||||
|
<p class="brand-overview__social-title">{{ socialTitle() || ('builder.brandOverviewTitle' | translate) }}</p>
|
||||||
|
@if (socialDescription(); as desc) {
|
||||||
|
<p class="brand-overview__social-desc">{{ desc }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<p class="brand-overview__social-empty">{{ 'builder.brandSocialEmpty' | translate }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
.brand-overview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
padding: var(--space-lg);
|
||||||
|
margin-bottom: var(--space-lg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-lg);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__ring {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 88px;
|
||||||
|
height: 88px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
span {
|
||||||
|
width: 68px;
|
||||||
|
height: 68px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__summary-text {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 200px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__next-step {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
|
||||||
|
&--done {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__last-saved {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__checklist {
|
||||||
|
list-style: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: var(--space-sm);
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-light);
|
||||||
|
|
||||||
|
.pi {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__check--ok {
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
.pi {
|
||||||
|
color: var(--success-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__card {
|
||||||
|
padding: var(--space-md);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 var(--space-sm);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__swatches {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(72px, 1fr));
|
||||||
|
gap: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__swatch {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__swatch-color {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__swatch-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-light);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__contrast-warning {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
margin: var(--space-sm) 0 0;
|
||||||
|
padding: var(--space-sm);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: color-mix(in srgb, var(--warning-color) 12%, transparent);
|
||||||
|
color: var(--warning-color);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__type-sample {
|
||||||
|
padding: var(--space-sm);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__type-heading {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__type-body {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__social-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__social-image {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1.91 / 1;
|
||||||
|
object-fit: cover;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__social-text {
|
||||||
|
padding: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__social-title {
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__social-desc {
|
||||||
|
margin: 2px 0 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__social-empty {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 639px) {
|
||||||
|
.brand-overview {
|
||||||
|
padding: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-overview__summary {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
|
||||||
|
import { TranslatePipe } from '../../../../i18n/translate.pipe';
|
||||||
|
import { LanguageService } from '../../../../services/language.service';
|
||||||
|
import { ProjectEditorFacade } from '../../facade/project-editor.facade';
|
||||||
|
import { contrastRatio } from './contrast.util';
|
||||||
|
|
||||||
|
interface BrandHealthItem {
|
||||||
|
labelKey: string;
|
||||||
|
ok: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-brand-overview',
|
||||||
|
standalone: true,
|
||||||
|
imports: [TranslatePipe],
|
||||||
|
templateUrl: './brand-overview.component.html',
|
||||||
|
styleUrl: './brand-overview.component.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class BrandOverviewComponent {
|
||||||
|
private readonly facade = inject(ProjectEditorFacade);
|
||||||
|
private readonly languageService = inject(LanguageService);
|
||||||
|
|
||||||
|
readonly bootstrap = this.facade.bootstrap;
|
||||||
|
readonly lastSavedAt = this.facade.lastSavedAt;
|
||||||
|
|
||||||
|
readonly branding = computed(() => this.bootstrap()?.branding ?? null);
|
||||||
|
readonly palette = computed(() => this.bootstrap()?.theme.palette ?? null);
|
||||||
|
readonly typography = computed(() => this.bootstrap()?.theme.typography ?? null);
|
||||||
|
|
||||||
|
readonly paletteSwatches = computed(() => {
|
||||||
|
const palette = this.palette();
|
||||||
|
if (!palette) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ key: 'primary', labelKey: 'builder.primaryColor', value: palette.primary },
|
||||||
|
{ key: 'secondary', labelKey: 'builder.secondaryColor', value: palette.secondary },
|
||||||
|
{ key: 'accent', labelKey: 'builder.brandAccentColor', value: palette.accent },
|
||||||
|
{ key: 'success', labelKey: 'builder.successColor', value: palette.success },
|
||||||
|
{ key: 'warning', labelKey: 'builder.warningColor', value: palette.warning },
|
||||||
|
{ key: 'danger', labelKey: 'builder.dangerColor', value: palette.danger },
|
||||||
|
{ key: 'backgroundPrimary', labelKey: 'builder.backgroundColor', value: palette.backgroundPrimary },
|
||||||
|
{ key: 'backgroundSecondary', labelKey: 'builder.surfaceColor', value: palette.backgroundSecondary },
|
||||||
|
{ key: 'textPrimary', labelKey: 'builder.textColor', value: palette.textPrimary },
|
||||||
|
{ key: 'border', labelKey: 'builder.brandBorderColor', value: palette.border },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
/** Real WCAG contrast math against the real configured colors - null when a value isn't a parseable hex, never fabricated. */
|
||||||
|
readonly textContrast = computed<number | null>(() => {
|
||||||
|
const palette = this.palette();
|
||||||
|
return palette ? contrastRatio(palette.textPrimary, palette.backgroundPrimary) : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly primaryContrast = computed<number | null>(() => {
|
||||||
|
const palette = this.palette();
|
||||||
|
return palette ? contrastRatio(palette.primary, palette.backgroundPrimary) : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly textContrastPasses = computed(() => {
|
||||||
|
const ratio = this.textContrast();
|
||||||
|
return ratio !== null && ratio >= 4.5;
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly healthItems = computed<BrandHealthItem[]>(() => {
|
||||||
|
const branding = this.branding();
|
||||||
|
if (!branding) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{ labelKey: 'builder.readinessLogoUploaded', ok: !!branding.logoUrl },
|
||||||
|
{ labelKey: 'builder.brandFaviconUploaded', ok: !!branding.faviconUrl },
|
||||||
|
{ labelKey: 'builder.brandColorsSelected', ok: !!this.palette() },
|
||||||
|
{ labelKey: 'builder.brandTypographyConfigured', ok: !!this.typography() },
|
||||||
|
{ labelKey: 'builder.brandSocialImageUploaded', ok: !!branding.socialImageUrl },
|
||||||
|
{ labelKey: 'builder.brandAccessibilityPassed', ok: this.textContrastPasses() },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly completionPercent = computed(() => {
|
||||||
|
const items = this.healthItems();
|
||||||
|
if (items.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Math.round((items.filter(i => i.ok).length / items.length) * 100);
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly ringBackground = computed(() => {
|
||||||
|
const pct = this.completionPercent() ?? 0;
|
||||||
|
return `conic-gradient(var(--primary-color) ${pct}%, var(--bg-tertiary) 0)`;
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly nextStepKey = computed<string | null>(() => {
|
||||||
|
const items = this.healthItems();
|
||||||
|
const missing = items.find(i => !i.ok);
|
||||||
|
return missing?.labelKey ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
readonly socialTitle = computed(() => this.bootstrap()?.seo.default.title || this.branding()?.brandName || null);
|
||||||
|
readonly socialDescription = computed(() => this.bootstrap()?.seo.default.description || null);
|
||||||
|
|
||||||
|
formattedLastSaved(): string | null {
|
||||||
|
const timestamp = this.lastSavedAt();
|
||||||
|
return timestamp ? new Date(timestamp).toLocaleString(this.languageService.currentLanguage()) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
/** WCAG 2.x relative-luminance contrast ratio between two hex colors. Pure, no fabrication - real math on real values. */
|
||||||
|
export function contrastRatio(hexA: string, hexB: string): number | null {
|
||||||
|
const lumA = relativeLuminance(hexA);
|
||||||
|
const lumB = relativeLuminance(hexB);
|
||||||
|
if (lumA === null || lumB === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const lighter = Math.max(lumA, lumB);
|
||||||
|
const darker = Math.min(lumA, lumB);
|
||||||
|
return (lighter + 0.05) / (darker + 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
function relativeLuminance(hex: string): number | null {
|
||||||
|
const rgb = hexToRgb(hex);
|
||||||
|
if (!rgb) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const [r, g, b] = rgb.map(channel => {
|
||||||
|
const c = channel / 255;
|
||||||
|
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||||
|
});
|
||||||
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hexToRgb(hex: string): [number, number, number] | null {
|
||||||
|
const match = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex.trim());
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return [parseInt(match[1], 16), parseInt(match[2], 16), parseInt(match[3], 16)];
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
@if (bootstrap(); as bootstrap) {
|
@if (bootstrap(); as bootstrap) {
|
||||||
|
<app-brand-overview />
|
||||||
<app-section-card [title]="'builder.branding' | translate">
|
<app-section-card [title]="'builder.branding' | translate">
|
||||||
<div class="editor-grid two">
|
<div class="editor-grid two">
|
||||||
<app-form-field [label]="'builder.logo' | translate" [hint]="'builder.logoDesc' | translate" [error]="fieldError('branding.logoUrl')">
|
<app-form-field [label]="'builder.logo' | translate" [hint]="'builder.logoDesc' | translate" [error]="fieldError('branding.logoUrl')">
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ import { FormFieldComponent } from '../../../shared/ui/form-field/form-field.com
|
|||||||
import { ButtonComponent } from '../../../shared/ui/button/button.component';
|
import { ButtonComponent } from '../../../shared/ui/button/button.component';
|
||||||
import { ImageFieldComponent } from '../../../shared/ui/image-field/image-field.component';
|
import { ImageFieldComponent } from '../../../shared/ui/image-field/image-field.component';
|
||||||
import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component';
|
import { SectionCardComponent } from '../../../shared/ui/section-card/section-card.component';
|
||||||
|
import { BrandOverviewComponent } from './brand/brand-overview.component';
|
||||||
|
|
||||||
type BrandingImageField = 'logoUrl' | 'logoCompactUrl' | 'faviconUrl' | 'socialImageUrl';
|
type BrandingImageField = 'logoUrl' | 'logoCompactUrl' | 'faviconUrl' | 'socialImageUrl';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-project-editor-branding-section',
|
selector: 'app-project-editor-branding-section',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [FormsModule, TranslatePipe, InputComponent, FormFieldComponent, ButtonComponent, ImageFieldComponent, SectionCardComponent],
|
imports: [FormsModule, TranslatePipe, InputComponent, FormFieldComponent, ButtonComponent, ImageFieldComponent, SectionCardComponent, BrandOverviewComponent],
|
||||||
templateUrl: './branding-section.component.html',
|
templateUrl: './branding-section.component.html',
|
||||||
styleUrls: ['./section.shared.scss'],
|
styleUrls: ['./section.shared.scss'],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
|||||||
@@ -762,6 +762,24 @@ export const en: Translations = {
|
|||||||
groupPreviewPurpose: 'Check your marketplace through a shopper\'s eyes before publishing - across screen sizes.',
|
groupPreviewPurpose: 'Check your marketplace through a shopper\'s eyes before publishing - across screen sizes.',
|
||||||
groupPreviewWhere: "This is a preview - shoppers never see it.",
|
groupPreviewWhere: "This is a preview - shoppers never see it.",
|
||||||
groupPreviewSkip: 'You can publish your changes without reviewing them first.',
|
groupPreviewSkip: 'You can publish your changes without reviewing them first.',
|
||||||
|
brandOverviewTitle: 'Brand identity',
|
||||||
|
brandOverviewSubtitle: 'The logo, colors, and fonts shoppers see on every page.',
|
||||||
|
brandOverviewComplete: 'Your brand is fully set up.',
|
||||||
|
brandLastModified: 'Last modified',
|
||||||
|
brandFaviconUploaded: 'Favicon uploaded',
|
||||||
|
brandColorsSelected: 'Brand colors selected',
|
||||||
|
brandTypographyConfigured: 'Typography configured',
|
||||||
|
brandSocialImageUploaded: 'Social share image uploaded',
|
||||||
|
brandAccessibilityPassed: 'Text contrast looks good',
|
||||||
|
brandColorsPreviewTitle: 'Marketplace colors',
|
||||||
|
brandTypographyPreviewTitle: 'Typography',
|
||||||
|
brandSocialPreviewTitle: 'How this looks when shared',
|
||||||
|
brandTypeSampleHeading: 'Sample heading',
|
||||||
|
brandTypeSampleBody: 'Sample body text the way shoppers will see it.',
|
||||||
|
brandContrastWarning: 'This text is hard to read against the background - consider higher-contrast colors. This will not block publishing.',
|
||||||
|
brandSocialEmpty: "No social share image yet - shared links will show a blank card until you add one.",
|
||||||
|
brandAccentColor: 'Accent',
|
||||||
|
brandBorderColor: 'Borders',
|
||||||
},
|
},
|
||||||
dashboard: {
|
dashboard: {
|
||||||
title: 'Dashboard',
|
title: 'Dashboard',
|
||||||
|
|||||||
@@ -762,6 +762,24 @@ export const hy: Translations = {
|
|||||||
groupPreviewPurpose: 'Ստուգեք ձեր մարկետփլեյսը գնորդի աչքերով նախքան հրապարակելը՝ տարբեր էկրանների վրա։',
|
groupPreviewPurpose: 'Ստուգեք ձեր մարկետփլեյսը գնորդի աչքերով նախքան հրապարակելը՝ տարբեր էկրանների վրա։',
|
||||||
groupPreviewWhere: 'Սա նախադիտում է՝ գնորդները այն երբեք չեն տեսնում։',
|
groupPreviewWhere: 'Սա նախադիտում է՝ գնորդները այն երբեք չեն տեսնում։',
|
||||||
groupPreviewSkip: 'Կարող եք հրապարակել փոփոխությունները առանց ստուգելու։',
|
groupPreviewSkip: 'Կարող եք հրապարակել փոփոխությունները առանց ստուգելու։',
|
||||||
|
brandOverviewTitle: 'Բրենդի ինքնություն',
|
||||||
|
brandOverviewSubtitle: 'Լոգոն, գույները և տառատեսակները, որոնք գնորդները տեսնում են յուրաքանչյուր էջում։',
|
||||||
|
brandOverviewComplete: 'Ձեր բրենդը ամբողջությամբ կարգավորված է։',
|
||||||
|
brandLastModified: 'Վերջին փոփոխությունը',
|
||||||
|
brandFaviconUploaded: 'Կայքի պատկերակը վերբեռնված է',
|
||||||
|
brandColorsSelected: 'Բրենդի գույները ընտրված են',
|
||||||
|
brandTypographyConfigured: 'Տառատեսակները կարգավորված են',
|
||||||
|
brandSocialImageUploaded: 'Սոցիալական ցանցերի պատկերը վերբեռնված է',
|
||||||
|
brandAccessibilityPassed: 'Տեքստի հակադրությունը լավ է',
|
||||||
|
brandColorsPreviewTitle: 'Մարկետփլեյսի գույները',
|
||||||
|
brandTypographyPreviewTitle: 'Տառատեսակներ',
|
||||||
|
brandSocialPreviewTitle: 'Ինչպես է սա երևում հղումը կիսելիս',
|
||||||
|
brandTypeSampleHeading: 'Վերնագրի օրինակ',
|
||||||
|
brandTypeSampleBody: 'Հիմնական տեքստի օրինակ, ինչպես գնորդները կտեսնեն։',
|
||||||
|
brandContrastWarning: 'Տեքստը ֆոնի վրա դժվար է կարդալ․ դիտարկեք ավելի հակադիր գույներ։ Սա չի արգելափակում հրապարակումը։',
|
||||||
|
brandSocialEmpty: 'Սոցիալական պատկեր դեռ վերբեռնված չէ․ հղումը կիսելիս գնորդները կտեսնեն դատարկ քարտ։',
|
||||||
|
brandAccentColor: 'Ընդգծող',
|
||||||
|
brandBorderColor: 'Եզրագծեր',
|
||||||
},
|
},
|
||||||
dashboard: {
|
dashboard: {
|
||||||
title: 'Կառավարման վահանակ',
|
title: 'Կառավարման վահանակ',
|
||||||
|
|||||||
@@ -762,6 +762,24 @@ export const ru: Translations = {
|
|||||||
groupPreviewPurpose: 'Проверьте маркетплейс глазами покупателя перед публикацией — на разных экранах.',
|
groupPreviewPurpose: 'Проверьте маркетплейс глазами покупателя перед публикацией — на разных экранах.',
|
||||||
groupPreviewWhere: 'Это предпросмотр — покупатели его не видят.',
|
groupPreviewWhere: 'Это предпросмотр — покупатели его не видят.',
|
||||||
groupPreviewSkip: 'Вы можете опубликовать изменения непроверенными.',
|
groupPreviewSkip: 'Вы можете опубликовать изменения непроверенными.',
|
||||||
|
brandOverviewTitle: 'Идентичность бренда',
|
||||||
|
brandOverviewSubtitle: 'Логотип, цвета и шрифты, которые видят покупатели на каждой странице.',
|
||||||
|
brandOverviewComplete: 'Бренд полностью настроен.',
|
||||||
|
brandLastModified: 'Последнее изменение',
|
||||||
|
brandFaviconUploaded: 'Иконка сайта загружена',
|
||||||
|
brandColorsSelected: 'Цвета бренда выбраны',
|
||||||
|
brandTypographyConfigured: 'Шрифты настроены',
|
||||||
|
brandSocialImageUploaded: 'Изображение для соцсетей загружено',
|
||||||
|
brandAccessibilityPassed: 'Контрастность текста в норме',
|
||||||
|
brandColorsPreviewTitle: 'Цвета маркетплейса',
|
||||||
|
brandTypographyPreviewTitle: 'Шрифты',
|
||||||
|
brandSocialPreviewTitle: 'Как это выглядит при публикации ссылки',
|
||||||
|
brandTypeSampleHeading: 'Пример заголовка',
|
||||||
|
brandTypeSampleBody: 'Пример основного текста, который видят покупатели.',
|
||||||
|
brandContrastWarning: 'Текст на фоне читается плохо — рассмотрите более контрастные цвета. Публикацию это не блокирует.',
|
||||||
|
brandSocialEmpty: 'Изображение для соцсетей ещё не загружено — при публикации ссылки покупатели увидят пустую карточку.',
|
||||||
|
brandAccentColor: 'Акцентный',
|
||||||
|
brandBorderColor: 'Границы',
|
||||||
},
|
},
|
||||||
dashboard: {
|
dashboard: {
|
||||||
title: 'Панель управления',
|
title: 'Панель управления',
|
||||||
|
|||||||
@@ -760,6 +760,24 @@ export interface Translations {
|
|||||||
groupPreviewPurpose: string;
|
groupPreviewPurpose: string;
|
||||||
groupPreviewWhere: string;
|
groupPreviewWhere: string;
|
||||||
groupPreviewSkip: string;
|
groupPreviewSkip: string;
|
||||||
|
brandOverviewTitle: string;
|
||||||
|
brandOverviewSubtitle: string;
|
||||||
|
brandOverviewComplete: string;
|
||||||
|
brandLastModified: string;
|
||||||
|
brandFaviconUploaded: string;
|
||||||
|
brandColorsSelected: string;
|
||||||
|
brandTypographyConfigured: string;
|
||||||
|
brandSocialImageUploaded: string;
|
||||||
|
brandAccessibilityPassed: string;
|
||||||
|
brandColorsPreviewTitle: string;
|
||||||
|
brandTypographyPreviewTitle: string;
|
||||||
|
brandSocialPreviewTitle: string;
|
||||||
|
brandTypeSampleHeading: string;
|
||||||
|
brandTypeSampleBody: string;
|
||||||
|
brandContrastWarning: string;
|
||||||
|
brandSocialEmpty: string;
|
||||||
|
brandAccentColor: string;
|
||||||
|
brandBorderColor: string;
|
||||||
};
|
};
|
||||||
dashboard: {
|
dashboard: {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user