Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 40 additions & 3 deletions app/renderer/branding.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ const BRAND_FONT_SUGGESTIONS = [
const BRAND_DEFAULT_COLORS = { primary: '#2563EB', accent: '#0EA5E9', text: '#18212B' };
// Mirrors DEFAULT_ANNOTATION_COLORS in core/branding.js: StepForge's own, used until a profile turns its own on.
const BRAND_DEFAULT_ANNOTATION_COLORS = { shapes: '#E5484D', numbers: '#1F6FEB', tooltips: '#111827' };
// Mirrors normalizeProfile's screenshots defaults in core/branding.js.
const BRAND_DEFAULT_SCREENSHOTS = { align: 'left', border: false, borderColor: '#D0D5DD', layout: 'stacked' };

// Mirrors MAX_CSS_CHARS in core/branding.js (a unit test keeps the two in step).
const BRAND_CSS_MAX_CHARS = 200000;
Expand All @@ -41,10 +43,10 @@ const BRAND_AI_REQUEST_MAX_CHARS = 500;

// What a profile changes in each export (the issue's support table).
const BRAND_SUPPORT = [
[t('HTML (simple and rich)'), t('Logo, banner, colors, fonts (uploaded fonts are embedded), cover style, footer text and your CSS.')],
[t('HTML (simple and rich)'), t('Logo, banner, colors, fonts (uploaded fonts are embedded), cover style, footer text, screenshot alignment, border and layout, and your CSS.')],
['GitHub Pages', t('Everything the HTML exports get.')],
['PDF', t('Logo, banner, colors, the closest built-in fonts, cover style, page header and footer with page numbers.')],
['Word', t('Styles with your fonts and colors, logo and banner on the cover, page header and footer with page numbers.')],
['PDF', t('Logo, banner, colors, the closest built-in fonts, cover style, page header and footer with page numbers, and screenshot alignment, border and layout.')],
['Word', t('Styles with your fonts and colors, logo and banner on the cover, page header and footer with page numbers, and screenshot alignment, border and layout.')],
['PowerPoint', t('A theme with your colors and fonts, a title slide in your cover style, logo, footer and slide numbers on every slide.')],
['Wiki.js', t('Logo, banner, header and footer text.')],
['Confluence', t('Logo, banner, header and footer text.')],
Expand All @@ -60,6 +62,17 @@ const BRAND_COVER_STYLES = [
{ value: 'band', label: t('Brand color band') },
];

const BRAND_IMAGE_ALIGNMENTS = [
{ value: 'left', label: t('Left') },
{ value: 'center', label: t('Center') },
{ value: 'right', label: t('Right') },
];

const BRAND_STEP_LAYOUTS = [
{ value: 'stacked', label: t('Stacked') },
{ value: 'side-by-side', label: t('Side by side') },
];

const BRAND_WATERMARK_POSITIONS = [
{ value: 'bottom-right', label: t('Bottom right') },
{ value: 'bottom-left', label: t('Bottom left') },
Expand Down Expand Up @@ -144,6 +157,13 @@ function brandThumb(asset, label) {
}

/** The read-only summary of a profile under the Active brand profile list. */
/** "Centered · Border #D0D5DD · Side by side", or "Left · Stacked" for the defaults. */
function screenshotSummary(shots = BRAND_DEFAULT_SCREENSHOTS) {
const align = BRAND_IMAGE_ALIGNMENTS.find((a) => a.value === shots.align)?.label || t('Left');
const layout = BRAND_STEP_LAYOUTS.find((l) => l.value === shots.layout)?.label || t('Stacked');
return [align, shots.border ? t('Border {color}', { color: shots.borderColor }) : '', layout].filter(Boolean).join(' · ');
}

function brandSummary(data) {
if (!data) {
return [el('p.setting-desc.brand-none', {},
Expand Down Expand Up @@ -177,6 +197,7 @@ function brandSummary(data) {
el('dt', {}, t('Cover style')), el('dd', {}, cover),
el('dt', {}, t('Header')), el('dd', {}, [profile.header.logo && profile.logo ? t('Logo') : '', profile.header.text].filter(Boolean).join(' · ') || t('None')),
el('dt', {}, t('Footer')), el('dd', {}, [profile.footer.text, profile.footer.pageNumbers ? t('Page numbers') : ''].filter(Boolean).join(' · ') || t('None')),
el('dt', {}, t('Screenshots')), el('dd', {}, screenshotSummary(profile.screenshots)),
el('dt', {}, t('Watermark')), el('dd', {}, profile.watermark.enabled ? t('Logo, {position}', { position: BRAND_WATERMARK_POSITIONS.find((p) => p.value === profile.watermark.position)?.label.toLowerCase() }) : t('Off')),
el('dt', {}, t('Annotations')), el('dd', {}, profile.annotations?.custom
? t('Shapes {shapes} · Number badges {numbers} · Tooltips {tooltips}', profile.annotations)
Expand Down Expand Up @@ -357,6 +378,7 @@ function showBrandProfileEditor(api, data = null) {
html: { css: '', colorScheme: 'light', ...(base?.html || {}) },
watermark: { enabled: false, position: 'bottom-right', opacity: 0.6, ...(base?.watermark || {}) },
annotations: { custom: false, ...BRAND_DEFAULT_ANNOTATION_COLORS, ...(base?.annotations || {}) },
screenshots: { ...BRAND_DEFAULT_SCREENSHOTS, ...(base?.screenshots || {}) },
};
// Errors from Preview and Save show in the footer beside those buttons.
const footerStatus = el('span.brand-footer-status.error', { role: 'alert' });
Expand Down Expand Up @@ -535,6 +557,16 @@ function showBrandProfileEditor(api, data = null) {
const opacityLabel = el('output', {}, `${Math.round(draft.watermark.opacity * 100)}%`);
opacity.addEventListener('input', () => { draft.watermark.opacity = Number(opacity.value); opacityLabel.textContent = `${Math.round(draft.watermark.opacity * 100)}%`; });

// Screenshots: alignment, border and step layout in PDF, Word and HTML.
const imageAlign = brandSelect(draft.screenshots.align, BRAND_IMAGE_ALIGNMENTS, { 'aria-label': t('Screenshot alignment') });
imageAlign.addEventListener('change', () => { draft.screenshots.align = imageAlign.value; });
const stepLayout = brandSelect(draft.screenshots.layout, BRAND_STEP_LAYOUTS, { 'aria-label': t('Step layout') });
stepLayout.addEventListener('change', () => { draft.screenshots.layout = stepLayout.value; });
const borderOn = el('input', { type: 'checkbox', checked: draft.screenshots.border });
const borderColorRow = colorRow(draft.screenshots, 'borderColor', t('Border color'), t('A thin line around each screenshot.'));
borderColorRow.hidden = !draft.screenshots.border;
borderOn.addEventListener('change', () => { draft.screenshots.border = borderOn.checked; borderColorRow.hidden = !borderOn.checked; });

// Advanced CSS
const css = el('textarea.brand-css-input', { rows: 7, spellcheck: false, placeholder: '.doc-header { padding: 2rem; }\n.step-head h2 { color: var(--brand-primary); }', 'aria-label': t('Advanced CSS') });
css.value = draft.html.css;
Expand Down Expand Up @@ -690,6 +722,11 @@ function showBrandProfileEditor(api, data = null) {
settingsCard(t('Layout'),
settingRow(t('Cover style'), t('The title page of PDF, Word and PowerPoint, the top of HTML pages and the GIF title card.'), cover),
settingRow(t('HTML color scheme'), t('Brand colors are usually picked for light pages.'), scheme)),
settingsCard(t('Screenshots'),
settingRow(t('Alignment'), t('Where screenshots and flowcharts narrower than the page sit in PDF, Word and HTML.'), imageAlign),
settingRow(t('Border around screenshots'), t('Sets screenshots apart from the page in PDF, Word and HTML.'), makeSwitch(borderOn, t('Border around screenshots'))),
borderColorRow,
settingRow(t('Step layout'), t('Side by side puts each screenshot on the left and its description on the right in PDF, Word and HTML. Steps without a description stay stacked.'), stepLayout)),
settingsCard(t('Header and footer'),
settingRow(t('Header text'), t('Top of every PDF and Word page; under the logo in HTML and wiki pages.'), textInput(draft.header, 'text', t('Acme Corp · IT Service Desk'), 160)),
settingRow(t('Logo in page header'), t('PDF and Word pages after the cover.'), toggle(draft.header, 'logo', t('Logo in page header'))),
Expand Down
30 changes: 28 additions & 2 deletions core/agent-brand-tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
const fs = require('node:fs');
const path = require('node:path');
const {
BrandingStore, normalizeProfile, checkFileSize, COVER_STYLES, WATERMARK_POSITIONS, COLOR_SCHEMES, MAX_CSS_CHARS,
BrandingStore, normalizeProfile, checkFileSize, COVER_STYLES, WATERMARK_POSITIONS, COLOR_SCHEMES, IMAGE_ALIGNMENTS, STEP_LAYOUTS, MAX_CSS_CHARS,
} = require('./branding');
const { STYLE_REFERENCE } = require('./ai-brand-css');
const { Settings } = require('./settings');
Expand Down Expand Up @@ -71,6 +71,19 @@ const PROFILE_FIELDS = {
},
additionalProperties: false,
},
screenshots: {
type: 'object',
description: 'How PDF, Word and HTML exports place step screenshots. align: left, center or right (also flowcharts). '
+ 'border: a thin line around each screenshot, in border_color. layout: stacked (screenshot under the text) or '
+ 'side-by-side (screenshot left, description right; steps without a description stay stacked).',
properties: {
align: { type: 'string', enum: IMAGE_ALIGNMENTS },
border: { type: 'boolean' },
border_color: colorSchema,
layout: { type: 'string', enum: STEP_LAYOUTS },
},
additionalProperties: false,
},
annotation_colors: {
type: 'object',
description: 'Colors new annotations start in, used only when custom is true; otherwise StepForge\'s (red shapes, blue number badges, dark tooltips).',
Expand Down Expand Up @@ -121,6 +134,12 @@ function createBrandTools({ settingsDir, requireEnabled, agentSettings, onChange
header: profile.header,
footer: { text: profile.footer.text, page_numbers: profile.footer.pageNumbers },
watermark: profile.watermark,
screenshots: {
align: profile.screenshots.align,
border: profile.screenshots.border,
border_color: profile.screenshots.borderColor,
layout: profile.screenshots.layout,
},
annotation_colors: profile.annotations,
css: profile.html.css,
updated_at: profile.updatedAt,
Expand Down Expand Up @@ -179,6 +198,13 @@ function createBrandTools({ settingsDir, requireEnabled, agentSettings, onChange
next.watermark.opacity = opacity;
}
}
if (has(fields, 'screenshots')) {
const shots = object(fields.screenshots, 'screenshots', ['align', 'border', 'border_color', 'layout']);
if (has(shots, 'align')) next.screenshots.align = choice(shots.align, IMAGE_ALIGNMENTS, 'screenshots.align');
if (has(shots, 'border')) next.screenshots.border = bool(shots.border, 'screenshots.border');
if (has(shots, 'border_color')) next.screenshots.borderColor = color(shots.border_color, 'screenshots.border_color');
if (has(shots, 'layout')) next.screenshots.layout = choice(shots.layout, STEP_LAYOUTS, 'screenshots.layout');
}
if (has(fields, 'annotation_colors')) {
const annotations = object(fields.annotation_colors, 'annotation_colors', ['custom', 'shapes', 'numbers', 'tooltips']);
if (has(annotations, 'custom')) next.annotations.custom = bool(annotations.custom, 'annotation_colors.custom');
Expand Down Expand Up @@ -270,7 +296,7 @@ function createBrandTools({ settingsDir, requireEnabled, agentSettings, onChange
},
{
name: 'update_brand_profile',
description: 'Change a brand profile. Leave a field out to keep it; in colors, fonts, header, footer, watermark and annotation_colors, '
description: 'Change a brand profile. Leave a field out to keep it; in colors, fonts, header, footer, watermark, screenshots and annotation_colors, '
+ 'only the keys you send change.',
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
inputSchema: {
Expand Down
15 changes: 15 additions & 0 deletions core/branding.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ const { sanitizeSvg, svgSize } = require('./svg-sanitize');
const COVER_STYLES = ['standard', 'band', 'centered'];
const WATERMARK_POSITIONS = ['bottom-right', 'bottom-left', 'top-right', 'top-left'];
const COLOR_SCHEMES = ['light', 'auto'];
const IMAGE_ALIGNMENTS = ['left', 'center', 'right'];
const STEP_LAYOUTS = ['stacked', 'side-by-side'];
const DEFAULT_BORDER_COLOR = '#D0D5DD';
const DEFAULT_COLORS = { primary: '#2563EB', accent: '#0EA5E9', text: '#18212B' };
// StepForge's own annotation colors: red shapes and click markers, blue
// number badges, near-black tooltips. A profile only changes them when its
Expand Down Expand Up @@ -149,6 +152,14 @@ function normalizeProfile(raw = {}, { id = raw.id, createdAt = raw.createdAt, up
position: oneOf(raw.watermark?.position, WATERMARK_POSITIONS, 'bottom-right'),
opacity: Number.isFinite(opacity) ? Math.round(Math.max(0.05, Math.min(1, opacity)) * 100) / 100 : 0.6,
},
// How PDF, Word and HTML place step screenshots. The defaults are what
// those exports did before these options existed.
screenshots: {
align: oneOf(raw.screenshots?.align, IMAGE_ALIGNMENTS, 'left'),
border: raw.screenshots?.border === true,
borderColor: normalizeColor(raw.screenshots?.borderColor, DEFAULT_BORDER_COLOR),
layout: oneOf(raw.screenshots?.layout, STEP_LAYOUTS, 'stacked'),
},
};
}

Expand Down Expand Up @@ -625,6 +636,7 @@ function loadBranding(resolved) {
cover: p.cover,
html: p.html,
watermark: p.watermark,
screenshots: p.screenshots,
};
}

Expand Down Expand Up @@ -670,6 +682,9 @@ module.exports = {
COVER_STYLES,
WATERMARK_POSITIONS,
COLOR_SCHEMES,
IMAGE_ALIGNMENTS,
STEP_LAYOUTS,
DEFAULT_BORDER_COLOR,
DEFAULT_COLORS,
DEFAULT_ANNOTATION_COLORS,
FONT_SUGGESTIONS,
Expand Down
19 changes: 16 additions & 3 deletions core/locales/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -853,10 +853,10 @@
'HTML (simple)': 'HTML (simple)',
'Image bundle': 'Paquete de imágenes',
'HTML (simple and rich)': 'HTML (simple y enriquecido)',
'Logo, banner, colors, fonts (uploaded fonts are embedded), cover style, footer text and your CSS.': 'Logotipo, banner, colores, fuentes (las fuentes subidas se incrustan), estilo de portada, texto del pie de página y tu CSS.',
"Logo, banner, colors, fonts (uploaded fonts are embedded), cover style, footer text, screenshot alignment, border and layout, and your CSS.": "Logotipo, banner, colores, fuentes (las fuentes subidas se incrustan), estilo de portada, texto del pie de página, alineación, borde y diseño de las capturas de pantalla, y tu CSS.",
'Everything the HTML exports get.': 'Todo lo que reciben las exportaciones HTML.',
'Logo, banner, colors, the closest built-in fonts, cover style, page header and footer with page numbers.': 'Logotipo, banner, colores, las fuentes integradas más parecidas, estilo de portada, encabezado y pie de página con números de página.',
'Styles with your fonts and colors, logo and banner on the cover, page header and footer with page numbers.': 'Estilos con tus fuentes y colores, logotipo y banner en la portada, encabezado y pie de página con números de página.',
"Logo, banner, colors, the closest built-in fonts, cover style, page header and footer with page numbers, and screenshot alignment, border and layout.": "Logotipo, banner, colores, las fuentes integradas más parecidas, estilo de portada, encabezado y pie de página con números de página, y alineación, borde y diseño de las capturas de pantalla.",
"Styles with your fonts and colors, logo and banner on the cover, page header and footer with page numbers, and screenshot alignment, border and layout.": "Estilos con tus fuentes y colores, logotipo y banner en la portada, encabezado y pie de página con números de página, y alineación, borde y diseño de las capturas de pantalla.",
'A theme with your colors and fonts, a title slide in your cover style, logo, footer and slide numbers on every slide.': 'Un tema con tus colores y fuentes, una diapositiva de título con tu estilo de portada, y logotipo, pie de página y números en cada diapositiva.',
'Logo, banner, header and footer text.': 'Logotipo, banner y textos de encabezado y pie de página.',
'Logo, banner, header and footer text, and the accent bar color.': 'Logotipo, banner, textos de encabezado y pie de página, y el color de la barra de acento.',
Expand Down Expand Up @@ -1703,6 +1703,19 @@
"Page layout": "Diseño de página",
"Contents": "Contenido",
"Screenshots": "Capturas de pantalla",
"Center": "Centro",
"Stacked": "Apilado",
"Side by side": "Lado a lado",
"Border {color}": "Borde {color}",
"Screenshot alignment": "Alineación de las capturas de pantalla",
"Step layout": "Diseño de los pasos",
"Border color": "Color del borde",
"A thin line around each screenshot.": "Una línea fina alrededor de cada captura de pantalla.",
"Alignment": "Alineación",
"Where screenshots and flowcharts narrower than the page sit in PDF, Word and HTML.": "Dónde se colocan las capturas de pantalla y los diagramas de flujo más estrechos que la página en PDF, Word y HTML.",
"Border around screenshots": "Borde alrededor de las capturas de pantalla",
"Sets screenshots apart from the page in PDF, Word and HTML.": "Separa visualmente las capturas de pantalla de la página en PDF, Word y HTML.",
"Side by side puts each screenshot on the left and its description on the right in PDF, Word and HTML. Steps without a description stay stacked.": "Lado a lado coloca cada captura de pantalla a la izquierda y su descripción a la derecha en PDF, Word y HTML. Los pasos sin descripción siguen apilados.",
"A4 (210 × 297 mm)": "A4 (210 × 297 mm)",
"US Letter (8.5 × 11 in)": "Carta de EE. UU. (8,5 × 11 pulgadas)",
"A3 (297 × 420 mm)": "A3 (297 × 420 mm)",
Expand Down
Loading
Loading