diff --git a/app/renderer/branding.js b/app/renderer/branding.js
index a0e7c51..f11d194 100644
--- a/app/renderer/branding.js
+++ b/app/renderer/branding.js
@@ -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;
@@ -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.')],
@@ -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') },
@@ -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', {},
@@ -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)
@@ -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' });
@@ -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;
@@ -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'))),
diff --git a/core/agent-brand-tools.js b/core/agent-brand-tools.js
index 0e411f5..47c6c17 100644
--- a/core/agent-brand-tools.js
+++ b/core/agent-brand-tools.js
@@ -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');
@@ -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).',
@@ -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,
@@ -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');
@@ -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: {
diff --git a/core/branding.js b/core/branding.js
index d4fca48..9fb757d 100644
--- a/core/branding.js
+++ b/core/branding.js
@@ -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
@@ -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'),
+ },
};
}
@@ -625,6 +636,7 @@ function loadBranding(resolved) {
cover: p.cover,
html: p.html,
watermark: p.watermark,
+ screenshots: p.screenshots,
};
}
@@ -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,
diff --git a/core/locales/es.js b/core/locales/es.js
index 4396c22..d4b5828 100644
--- a/core/locales/es.js
+++ b/core/locales/es.js
@@ -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.',
@@ -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)",
diff --git a/core/locales/fr.js b/core/locales/fr.js
index fbf242b..d0b5439 100644
--- a/core/locales/fr.js
+++ b/core/locales/fr.js
@@ -853,10 +853,10 @@
'HTML (simple)': 'HTML (simple)',
'Image bundle': 'Lot d’images',
'HTML (simple and rich)': 'HTML (simple et enrichi)',
- 'Logo, banner, colors, fonts (uploaded fonts are embedded), cover style, footer text and your CSS.': 'Logo, bannière, couleurs, polices (les polices envoyées sont intégrées), style de couverture, texte de pied de page et votre CSS.',
+ "Logo, banner, colors, fonts (uploaded fonts are embedded), cover style, footer text, screenshot alignment, border and layout, and your CSS.": "Logo, bannière, couleurs, polices (les polices envoyées sont intégrées), style de couverture, texte de pied de page, alignement, bordure et disposition des captures d’écran, et votre CSS.",
'Everything the HTML exports get.': 'Tout ce que reçoivent les exports HTML.',
- 'Logo, banner, colors, the closest built-in fonts, cover style, page header and footer with page numbers.': 'Logo, bannière, couleurs, polices intégrées les plus proches, style de couverture, en-tête et pied de page avec numéros de page.',
- 'Styles with your fonts and colors, logo and banner on the cover, page header and footer with page numbers.': 'Styles avec vos polices et couleurs, logo et bannière sur la couverture, en-tête et pied de page avec numéros de page.',
+ "Logo, banner, colors, the closest built-in fonts, cover style, page header and footer with page numbers, and screenshot alignment, border and layout.": "Logo, bannière, couleurs, polices intégrées les plus proches, style de couverture, en-tête et pied de page avec numéros de page, et alignement, bordure et disposition des captures d’écran.",
+ "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.": "Styles avec vos polices et couleurs, logo et bannière sur la couverture, en-tête et pied de page avec numéros de page, et alignement, bordure et disposition des captures d’écran.",
'A theme with your colors and fonts, a title slide in your cover style, logo, footer and slide numbers on every slide.': 'Un thème avec vos couleurs et polices, une diapositive de titre dans votre style de couverture, et logo, pied de page et numéros sur chaque diapositive.',
'Logo, banner, header and footer text.': 'Logo, bannière, textes d’en-tête et de pied de page.',
'Logo, banner, header and footer text, and the accent bar color.': 'Logo, bannière, textes d’en-tête et de pied de page, et couleur de la barre d’accent.',
@@ -1703,6 +1703,19 @@
"Page layout": "Mise en page",
"Contents": "Contenu",
"Screenshots": "Captures d’écran",
+ "Center": "Centre",
+ "Stacked": "Empilée",
+ "Side by side": "Côte à côte",
+ "Border {color}": "Bordure {color}",
+ "Screenshot alignment": "Alignement des captures d’écran",
+ "Step layout": "Disposition des étapes",
+ "Border color": "Couleur de bordure",
+ "A thin line around each screenshot.": "Un trait fin autour de chaque capture d’écran.",
+ "Alignment": "Alignement",
+ "Where screenshots and flowcharts narrower than the page sit in PDF, Word and HTML.": "Position des captures d’écran et des organigrammes moins larges que la page en PDF, Word et HTML.",
+ "Border around screenshots": "Bordure autour des captures d’écran",
+ "Sets screenshots apart from the page in PDF, Word and HTML.": "Détache les captures d’écran de la page en PDF, Word et 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.": "Côte à côte place chaque capture d’écran à gauche et sa description à droite en PDF, Word et HTML. Les étapes sans description restent empilées.",
"A4 (210 × 297 mm)": "A4 (210 × 297 mm)",
"US Letter (8.5 × 11 in)": "Lettre US (8,5 × 11 pouces)",
"A3 (297 × 420 mm)": "A3 (297 × 420 mm)",
diff --git a/core/locales/ja.js b/core/locales/ja.js
index f00ee05..378e165 100644
--- a/core/locales/ja.js
+++ b/core/locales/ja.js
@@ -853,10 +853,10 @@
'HTML (simple)': 'HTML(シンプル)',
'Image bundle': '画像セット',
'HTML (simple and rich)': 'HTML(シンプルとリッチ)',
- 'Logo, banner, colors, fonts (uploaded fonts are embedded), cover style, footer text and your CSS.': 'ロゴ、バナー、色、フォント(アップロードしたフォントは埋め込まれます)、表紙のスタイル、フッターのテキスト、独自の CSS。',
+ "Logo, banner, colors, fonts (uploaded fonts are embedded), cover style, footer text, screenshot alignment, border and layout, and your CSS.": "ロゴ、バナー、色、フォント(アップロードしたフォントは埋め込まれます)、表紙のスタイル、フッターのテキスト、スクリーンショットの配置・枠線・レイアウト、独自の CSS。",
'Everything the HTML exports get.': 'HTML エクスポートと同じすべての項目。',
- 'Logo, banner, colors, the closest built-in fonts, cover style, page header and footer with page numbers.': 'ロゴ、バナー、色、最も近い組み込みフォント、表紙のスタイル、ページ番号付きのヘッダーとフッター。',
- 'Styles with your fonts and colors, logo and banner on the cover, page header and footer with page numbers.': 'フォントと色を使ったスタイル、表紙のロゴとバナー、ページ番号付きのヘッダーとフッター。',
+ "Logo, banner, colors, the closest built-in fonts, cover style, page header and footer with page numbers, and screenshot alignment, border and layout.": "ロゴ、バナー、色、最も近い組み込みフォント、表紙のスタイル、ページ番号付きのヘッダーとフッター、スクリーンショットの配置・枠線・レイアウト。",
+ "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.": "フォントと色を使ったスタイル、表紙のロゴとバナー、ページ番号付きのヘッダーとフッター、スクリーンショットの配置・枠線・レイアウト。",
'A theme with your colors and fonts, a title slide in your cover style, logo, footer and slide numbers on every slide.': '色とフォントを使ったテーマ、表紙スタイルのタイトルスライド、すべてのスライドのロゴ、フッター、スライド番号。',
'Logo, banner, header and footer text.': 'ロゴ、バナー、ヘッダーとフッターのテキスト。',
'Logo, banner, header and footer text, and the accent bar color.': 'ロゴ、バナー、ヘッダーとフッターのテキスト、アクセントバーの色。',
@@ -1703,6 +1703,19 @@
"Page layout": "ページレイアウト",
"Contents": "内容",
"Screenshots": "スクリーンショット",
+ "Center": "中央",
+ "Stacked": "縦に並べる",
+ "Side by side": "横に並べる",
+ "Border {color}": "枠線 {color}",
+ "Screenshot alignment": "スクリーンショットの配置",
+ "Step layout": "ステップのレイアウト",
+ "Border color": "枠線の色",
+ "A thin line around each screenshot.": "各スクリーンショットを囲む細い線。",
+ "Alignment": "配置",
+ "Where screenshots and flowcharts narrower than the page sit in PDF, Word and HTML.": "PDF、Word、HTML で、ページより幅の狭いスクリーンショットとフローチャートを置く位置。",
+ "Border around screenshots": "スクリーンショットの枠線",
+ "Sets screenshots apart from the page in PDF, Word and HTML.": "PDF、Word、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.": "横に並べると、PDF、Word、HTML で各スクリーンショットが左、説明が右になります。説明のないステップは縦に並んだままです。",
"A4 (210 × 297 mm)": "A4(210 × 297 mm)",
"US Letter (8.5 × 11 in)": "米国レター(8.5 × 11インチ)",
"A3 (297 × 420 mm)": "A3(297 × 420 mm)",
diff --git a/core/locales/ru.js b/core/locales/ru.js
index f1deb7c..0166e49 100644
--- a/core/locales/ru.js
+++ b/core/locales/ru.js
@@ -853,10 +853,10 @@
'HTML (simple)': 'HTML (простой)',
'Image bundle': 'Набор изображений',
'HTML (simple and rich)': 'HTML (простой и расширенный)',
- 'Logo, banner, colors, fonts (uploaded fonts are embedded), cover style, footer text and your CSS.': 'Логотип, баннер, цвета, шрифты (загруженные шрифты встраиваются), стиль обложки, текст нижнего колонтитула и ваш CSS.',
+ "Logo, banner, colors, fonts (uploaded fonts are embedded), cover style, footer text, screenshot alignment, border and layout, and your CSS.": "Логотип, баннер, цвета, шрифты (загруженные шрифты встраиваются), стиль обложки, текст нижнего колонтитула, выравнивание, рамка и расположение снимков экрана, а также ваш CSS.",
'Everything the HTML exports get.': 'Всё то же, что и в экспорте HTML.',
- 'Logo, banner, colors, the closest built-in fonts, cover style, page header and footer with page numbers.': 'Логотип, баннер, цвета, ближайшие встроенные шрифты, стиль обложки, колонтитулы с номерами страниц.',
- 'Styles with your fonts and colors, logo and banner on the cover, page header and footer with page numbers.': 'Стили с вашими шрифтами и цветами, логотип и баннер на обложке, колонтитулы с номерами страниц.',
+ "Logo, banner, colors, the closest built-in fonts, cover style, page header and footer with page numbers, and screenshot alignment, border and layout.": "Логотип, баннер, цвета, ближайшие встроенные шрифты, стиль обложки, колонтитулы с номерами страниц, а также выравнивание, рамка и расположение снимков экрана.",
+ "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.": "Стили с вашими шрифтами и цветами, логотип и баннер на обложке, колонтитулы с номерами страниц, а также выравнивание, рамка и расположение снимков экрана.",
'A theme with your colors and fonts, a title slide in your cover style, logo, footer and slide numbers on every slide.': 'Тема с вашими цветами и шрифтами, титульный слайд в стиле обложки, логотип, нижний колонтитул и номера на каждом слайде.',
'Logo, banner, header and footer text.': 'Логотип, баннер, текст верхнего и нижнего колонтитулов.',
'Logo, banner, header and footer text, and the accent bar color.': 'Логотип, баннер, текст колонтитулов и цвет акцентной полосы.',
@@ -1703,6 +1703,19 @@
"Page layout": "Макет страницы",
"Contents": "Содержимое",
"Screenshots": "Снимки экрана",
+ "Center": "По центру",
+ "Stacked": "Друг под другом",
+ "Side by side": "Рядом",
+ "Border {color}": "Рамка {color}",
+ "Screenshot alignment": "Выравнивание снимков экрана",
+ "Step layout": "Расположение шагов",
+ "Border color": "Цвет рамки",
+ "A thin line around each screenshot.": "Тонкая линия вокруг каждого снимка экрана.",
+ "Alignment": "Выравнивание",
+ "Where screenshots and flowcharts narrower than the page sit in PDF, Word and HTML.": "Где располагаются снимки экрана и блок-схемы, которые уже страницы, в PDF, Word и HTML.",
+ "Border around screenshots": "Рамка вокруг снимков экрана",
+ "Sets screenshots apart from the page in PDF, Word and HTML.": "Отделяет снимки экрана от страницы в PDF, Word и 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.": "В режиме «Рядом» снимок экрана находится слева, а его описание справа в PDF, Word и HTML. Шаги без описания остаются друг под другом.",
"A4 (210 × 297 mm)": "A4 (210 × 297 мм)",
"US Letter (8.5 × 11 in)": "US Letter (8,5 × 11 дюймов)",
"A3 (297 × 420 mm)": "A3 (297 × 420 мм)",
diff --git a/core/locales/zh-CN.js b/core/locales/zh-CN.js
index b3d5e3a..25063b1 100644
--- a/core/locales/zh-CN.js
+++ b/core/locales/zh-CN.js
@@ -853,10 +853,10 @@
'HTML (simple)': 'HTML(简单)',
'Image bundle': '图片包',
'HTML (simple and rich)': 'HTML(简单和丰富)',
- 'Logo, banner, colors, fonts (uploaded fonts are embedded), cover style, footer text and your CSS.': '徽标、横幅、颜色、字体(上传的字体会被嵌入)、封面样式、页脚文本和你的 CSS。',
+ "Logo, banner, colors, fonts (uploaded fonts are embedded), cover style, footer text, screenshot alignment, border and layout, and your CSS.": "徽标、横幅、颜色、字体(上传的字体会被嵌入)、封面样式、页脚文本、截图的对齐方式、边框和布局,以及你的 CSS。",
'Everything the HTML exports get.': '与 HTML 导出相同的全部内容。',
- 'Logo, banner, colors, the closest built-in fonts, cover style, page header and footer with page numbers.': '徽标、横幅、颜色、最接近的内置字体、封面样式、带页码的页眉和页脚。',
- 'Styles with your fonts and colors, logo and banner on the cover, page header and footer with page numbers.': '使用你的字体和颜色的样式、封面上的徽标和横幅、带页码的页眉和页脚。',
+ "Logo, banner, colors, the closest built-in fonts, cover style, page header and footer with page numbers, and screenshot alignment, border and layout.": "徽标、横幅、颜色、最接近的内置字体、封面样式、带页码的页眉和页脚,以及截图的对齐方式、边框和布局。",
+ "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.": "使用你的字体和颜色的样式、封面上的徽标和横幅、带页码的页眉和页脚,以及截图的对齐方式、边框和布局。",
'A theme with your colors and fonts, a title slide in your cover style, logo, footer and slide numbers on every slide.': '使用你的颜色和字体的主题、采用你封面样式的标题幻灯片,以及每张幻灯片上的徽标、页脚和编号。',
'Logo, banner, header and footer text.': '徽标、横幅、页眉和页脚文本。',
'Logo, banner, header and footer text, and the accent bar color.': '徽标、横幅、页眉和页脚文本,以及强调条颜色。',
@@ -1703,6 +1703,19 @@
"Page layout": "页面布局",
"Contents": "内容",
"Screenshots": "截图",
+ "Center": "居中",
+ "Stacked": "上下排列",
+ "Side by side": "并排",
+ "Border {color}": "边框 {color}",
+ "Screenshot alignment": "截图对齐方式",
+ "Step layout": "步骤布局",
+ "Border color": "边框颜色",
+ "A thin line around each screenshot.": "每张截图周围的一条细线。",
+ "Alignment": "对齐方式",
+ "Where screenshots and flowcharts narrower than the page sit in PDF, Word and HTML.": "在 PDF、Word 和 HTML 中,比页面窄的截图和流程图的位置。",
+ "Border around screenshots": "截图边框",
+ "Sets screenshots apart from the page in PDF, Word and HTML.": "在 PDF、Word 和 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.": "并排时,在 PDF、Word 和 HTML 中每张截图在左、说明在右。没有说明的步骤仍上下排列。",
"A4 (210 × 297 mm)": "A4(210 × 297 毫米)",
"US Letter (8.5 × 11 in)": "美国信纸(8.5 × 11 英寸)",
"A3 (297 × 420 mm)": "A3(297 × 420 毫米)",
diff --git a/docs/BRANDING.md b/docs/BRANDING.md
index 82d3eff..b426c7e 100644
--- a/docs/BRANDING.md
+++ b/docs/BRANDING.md
@@ -53,10 +53,10 @@ there, or one saved in an export template, takes their place.
| Export | What it gets |
| --- | --- |
-| HTML (simple and rich) | Logo, banner, header text, colors, fonts (uploaded font files are built in), cover style, footer text and your [Advanced CSS](#advanced-css) |
+| HTML (simple and rich) | Logo, banner, header text, colors, fonts (uploaded font files are built in), cover style, footer text, [screenshot options](#screenshots) and your [Advanced CSS](#advanced-css) |
| GitHub Pages | Everything the HTML exports get |
-| PDF | Logo and banner on the cover in its style, colors, the closest built-in fonts, a header (logo and text) and footer (text and "Page 2 of 9") on every page after the cover |
-| Word | Normal and Heading styles in your fonts and colors, logo and banner on the cover, a header and footer with page numbers on every page after the cover |
+| PDF | Logo and banner on the cover in its style, colors, the closest built-in fonts, a header (logo and text) and footer (text and "Page 2 of 9") on every page after the cover, and [screenshot options](#screenshots) |
+| Word | Normal and Heading styles in your fonts and colors, logo and banner on the cover, a header and footer with page numbers on every page after the cover, and [screenshot options](#screenshots) |
| PowerPoint | A theme with your colors and fonts, a title slide in your cover style, and the logo, footer text and slide numbers on every slide after it |
| Wiki.js, Confluence | Logo, banner, header and footer text. Publishing uploads the images too |
| Markdown | Logo, banner, header and footer text, and the color of the accent line |
@@ -76,6 +76,24 @@ A few things to know:
- A watermark picked in the Export dialog (image bundle) replaces the
profile's.
+## Screenshots
+
+The **Screenshots** card sets how PDF, Word and HTML exports (and GitHub
+Pages) place each step's screenshot. Every option starts at what exports
+always did.
+
+- **Alignment**: **Left**, **Center** or **Right**. It moves screenshots and
+ flowcharts that are narrower than the page, such as a tall screenshot in a
+ PDF or one smaller than the **Screenshot width** in Word.
+- **Border around screenshots**: a thin line in the **Border color** around
+ each screenshot. HTML pages always draw a light border; this sets its color.
+- **Step layout**: **Stacked** puts the screenshot under the step's text.
+ **Side by side** puts the screenshot on the left and the description on the
+ right (55% and 45% of the width). Callouts keep their place: one placed
+ after the description sits under it, one placed before the image sits above
+ the screenshot. Steps without a description or a screenshot stay stacked.
+ HTML pages go back to one column on narrow screens.
+
## Advanced CSS
Advanced CSS styles the **HTML exports and GitHub Pages** only. Everything else
diff --git a/exporters/branding.js b/exporters/branding.js
index c80f098..2bb9e3e 100644
--- a/exporters/branding.js
+++ b/exporters/branding.js
@@ -105,8 +105,23 @@ function overlayWatermark(img, mark, { position = 'bottom-right', opacity = 0.6
raster.drawImage(img, scaled, Math.max(0, x), Math.max(0, y));
}
+/**
+ * How the brand places step screenshots (core/branding.js `screenshots`);
+ * without a profile, the way exports always placed them.
+ */
+function screenshotOptions(ast) {
+ const shots = ast?.branding?.screenshots;
+ return {
+ align: shots?.align || 'left',
+ border: shots?.border === true,
+ borderColor: shots?.borderColor || '#D0D5DD',
+ layout: shots?.layout || 'stacked',
+ };
+}
+
module.exports = {
withBrand,
+ screenshotOptions,
brandRaster,
fitSize,
fitRaster,
diff --git a/exporters/docx.js b/exporters/docx.js
index cedf253..183c32a 100644
--- a/exporters/docx.js
+++ b/exporters/docx.js
@@ -8,7 +8,7 @@ const { encodePng } = require('../core/png');
const { guideSlug, renderAllImages, LEVEL_LABEL, stepContentGroups, codeBlockText, wordCodeRuns, wordCodeShading, flowchartImageKey, flowchartAlt } = require('./common');
const { highlightCode } = require('../core/code-highlight');
const { guideMetaLines, guideSummary, tocEntries } = require('./document-layout');
-const { fitSize, textOn } = require('./branding');
+const { fitSize, textOn, screenshotOptions } = require('./branding');
/**
* DOCX exporter: WordprocessingML built directly (no dependency), one
@@ -60,7 +60,8 @@ function run(text, { bold = false, size = 22, font = '', color = '' } = {}) {
).join('');
}
-function drawing(relId, widthPx, heightPx, maxWidthTwips, alt, name = `Screenshot ${relId}`) {
+/** An inline picture; `border` is a hex color for a thin line around it. */
+function drawing(relId, widthPx, heightPx, maxWidthTwips, alt, name = `Screenshot ${relId}`, { border = '' } = {}) {
// scale to maxWidth (twips -> px at 96dpi: twips/15)
const maxWpx = maxWidthTwips / 15;
let w = widthPx, h = heightPx;
@@ -74,7 +75,9 @@ function drawing(relId, widthPx, heightPx, maxWidthTwips, alt, name = `Screensho
`
Click the gear icon in the corner.
' }, + encodePng(tall), { width: 200, height: 600 }, { position: 0 }); + const wide = raster.createImage(320, 200, [200, 120, 40, 255]); + store.addStep(guide.guideId, { title: 'Save' }, encodePng(wide), { width: 320, height: 200 }, { position: 1 }); + store.addStep(guide.guideId, { + title: 'Confirm', + descriptionHtml: 'Choose Confirm.
', + textBlocks: [ + { position: 'after-description', level: 'warn', title: 'Check who is asking', descriptionHtml: 'Verify identity.
' }, + { position: 'before-image', level: 'info', title: 'Look here', descriptionHtml: 'The button is at the bottom.
' }, + ], + }, encodePng(wide), { width: 320, height: 200 }, { position: 2 }); + const brands = new BrandingStore(path.join(root, 'settings')); + const profile = brands.save({ name: 'Acme', screenshots }); + return { + root, + ast: buildRenderAst(store, guide.guideId, { now: NOW, branding: brands.resolve(profile.id) }), + plain: buildRenderAst(store, guide.guideId, { now: NOW }), + }; +} + +function pdfPages(file) { + const buf = fs.readFileSync(file); + const text = buf.toString('latin1'); + const out = []; + const re = /\d+ 0 obj\n<< \/Filter \/FlateDecode \/Length (\d+) >>\nstream\n/g; + let m; + while ((m = re.exec(text)) !== null) { + const start = m.index + m[0].length; + out.push(zlib.inflateSync(buf.subarray(start, start + Number(m[1]))).toString('latin1')); + } + return out; +} + +/** Every image drawn on the PDF's pages: { x, y, w, h } in points. */ +function pdfImages(file) { + return pdfPages(file).flatMap((page) => [...page.matchAll(/q ([\d.]+) 0 0 ([\d.]+) ([\d.]+) ([\d.]+) cm \/Im\d+ Do Q/g)] + .map((m) => ({ w: Number(m[1]), h: Number(m[2]), x: Number(m[3]), y: Number(m[4]) }))); +} + +const docxXml = (file) => unzipSync(fs.readFileSync(file)).find((e) => e.name === 'word/document.xml').data.toString('utf8'); + +test('screenshot options: off by default, and invalid values fall back to the defaults', () => { + assert.deepEqual(normalizeProfile({}).screenshots, { align: 'left', border: false, borderColor: '#D0D5DD', layout: 'stacked' }); + assert.deepEqual(normalizeProfile({ screenshots: { align: 'middle', border: 'yes', borderColor: 'red', layout: 'grid' } }).screenshots, + { align: 'left', border: false, borderColor: '#D0D5DD', layout: 'stacked' }); + assert.deepEqual(normalizeProfile({ screenshots: { align: 'center', border: true, borderColor: '#173f7a', layout: 'side-by-side' } }).screenshots, + { align: 'center', border: true, borderColor: '#173F7A', layout: 'side-by-side' }); +}); + +test('screenshot options: a profile with the defaults exports exactly what no options did', (t) => { + const { root, ast } = fixture(t, undefined); + const { ast: explicit } = fixture(t, { align: 'left', border: false, layout: 'stacked' }); + const pdfA = pdfPages(exportPdf(ast, path.join(root, 'a')).file); + const pdfB = pdfPages(exportPdf(explicit, path.join(root, 'b')).file); + assert.deepEqual(pdfA, pdfB); + const doc = docxXml(exportDocx(ast, path.join(root, 'docx')).file); + assert.ok(!doc.includes('Click the gear icon/);
+ assert.equal((html.match(/class="step-split"/g) || []).length, 2, 'only steps with a description are split');
+ const third = html.slice(html.lastIndexOf('class="step-split"'));
+ const media = third.slice(third.indexOf('split-media'), third.indexOf('split-text'));
+ const text = third.slice(third.indexOf('split-text'));
+ assert.ok(media.indexOf('Look here') >= 0 && media.indexOf('Look here') < media.indexOf('