diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2f8c94d..121b408 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -51,6 +51,7 @@
**Page & layout**
- Page margins (cm) and page orientation (portrait / landscape)
+- Multi-column (newspaper) layout: 1–3 columns with adjustable gap, applied to the whole document (no selection) or to selected paragraphs, Word-style — text fills column 1 to the page bottom, then column 2, and flows across pages with mid-paragraph line breaks; a section followed by other content balances its columns. Round-trips to ODF `text:section`/`style:columns` and DOCX continuous sections with `w:cols`
- Headers & footers with page-number / page-count fields and configurable edge distances
- Zoom (20–300 %)
- Show formatting marks (spaces, tabs, paragraph marks)
@@ -81,7 +82,6 @@
Planned word-processor features (to match Word/LibreOffice):
- Footnotes / endnotes (currently dropped on import)
- Clear formatting (remove all marks)
-- Multi-column (newspaper) layout
- Comments / annotations (currently dropped on import)
- Horizontal rule
- Page numbering options (start value / format) + different first-page header
diff --git a/src/App.svelte b/src/App.svelte
index 33b2fb8..f0da589 100644
--- a/src/App.svelte
+++ b/src/App.svelte
@@ -12,6 +12,7 @@
import { importOdt } from './lib/import/odt';
import { importDocx } from './lib/import/docx';
import { getPageBreakDebug } from './lib/editor/extensions/pageBreaks';
+ import { getColumnsFlowDebug } from './lib/editor/extensions/columnsFlow';
import { getColorDebug } from './lib/utils/colorDebug';
import { countText, type TextStats } from './lib/utils/wordCount';
import { loadTheme, saveTheme, applyTheme, loadToolbarExpanded, saveToolbarExpanded, loadFormattingMarks, saveFormattingMarks, type ThemeMode } from './lib/storage/theme';
@@ -557,6 +558,7 @@
zoom,
doc: editor.getJSON(),
pageBreaks: snapshot,
+ columnsFlow: getColumnsFlowDebug(editor.view),
colors: getColorDebug(editor),
};
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
diff --git a/src/lib/components/ToolbarExpanded.svelte b/src/lib/components/ToolbarExpanded.svelte
index 4fc025b..8d70c44 100644
--- a/src/lib/components/ToolbarExpanded.svelte
+++ b/src/lib/components/ToolbarExpanded.svelte
@@ -16,6 +16,7 @@
import type { Orientation } from '../storage/pageOrientation';
import { DEFAULT_HF_DISTANCES, clampHfDistance, type HfDistances } from '../storage/headerFooter';
import { listContext } from '../editor/extensions/indent';
+ import { findColumns, DEFAULT_COLUMN_GAP_CM } from '../editor/extensions/columns';
import { t } from '../i18n/i18n.svelte';
import { withShortcut } from '../i18n/shortcut';
@@ -612,6 +613,7 @@
lineHeightOpen = false;
fontColorOpen = false;
highlightColorOpen = false;
+ columnsOpen = false;
layoutOpen = !layoutOpen;
if (layoutOpen) {
syncMarginInputs();
@@ -635,6 +637,7 @@
sizeOpen = false;
sizeInputFocused = false;
lineHeightOpen = false;
+ columnsOpen = false;
if (which === 'font') highlightColorOpen = false;
else fontColorOpen = false;
}
@@ -652,6 +655,7 @@
highlightColorOpen = false;
layoutOpen = false;
specialCharOpen = false;
+ columnsOpen = false;
}
function insertTable(rows: number, cols: number, range: { from: number; to: number }) {
@@ -665,6 +669,86 @@
editor.chain().focus().setTableOfContents().run();
}
+ // --- Multi-column section (columns.ts) ---
+ let columnsOpen = $state(false);
+ let columnGapInput = $state('');
+
+ // Count/gap of the columns section around the cursor; count 1 = not in one.
+ let colState = $derived.by(() => {
+ if (tick < 0 || !editor) return { count: 1, gapCm: DEFAULT_COLUMN_GAP_CM, inColumns: false };
+ const found = findColumns(editor.state);
+ return found
+ ? { count: found.node.attrs.count as number, gapCm: found.node.attrs.gapCm as number, inColumns: true }
+ : { count: 1, gapCm: DEFAULT_COLUMN_GAP_CM, inColumns: false };
+ });
+
+ function openColumnsPicker() {
+ fontOpen = false;
+ sizeOpen = false;
+ sizeInputFocused = false;
+ lineHeightOpen = false;
+ fontColorOpen = false;
+ highlightColorOpen = false;
+ layoutOpen = false;
+ tableOpen = false;
+ specialCharOpen = false;
+ columnsOpen = !columnsOpen;
+ if (columnsOpen) columnGapInput = fmtCm(colState.gapCm);
+ }
+
+ function columnsClickOutside(node: HTMLElement) {
+ function handler(e: MouseEvent) {
+ if (!node.contains(e.target as Node)) columnsOpen = false;
+ }
+ window.addEventListener('mousedown', handler);
+ return { destroy() { window.removeEventListener('mousedown', handler); } };
+ }
+
+ // The current gap straight from the document (colState may not have recomputed yet
+ // within the same handler after a command ran).
+ function liveColumnGap(): number {
+ const found = editor ? findColumns(editor.state) : null;
+ return (found?.node.attrs.gapCm as number | undefined) ?? DEFAULT_COLUMN_GAP_CM;
+ }
+
+ function applyColumnCount(n: number) {
+ if (!editor || hfActive) return;
+ editor.chain().focus().setColumns(n).run();
+ columnGapInput = fmtCm(liveColumnGap());
+ }
+
+ function setColumnGap(value: number) {
+ if (!editor) return;
+ editor.chain().focus().setColumnGap(value).run();
+ columnGapInput = fmtCm(liveColumnGap());
+ }
+
+ function commitColumnGapInput() {
+ const n = parseFloat(columnGapInput);
+ if (Number.isNaN(n)) {
+ columnGapInput = fmtCm(liveColumnGap()); // revert invalid input
+ return;
+ }
+ setColumnGap(n);
+ }
+
+ function onColumnGapKeydown(e: KeyboardEvent) {
+ if (e.key === 'Enter') {
+ e.preventDefault();
+ commitColumnGapInput();
+ (e.target as HTMLInputElement).blur();
+ } else if (e.key === 'Escape') {
+ columnGapInput = fmtCm(liveColumnGap());
+ (e.target as HTMLInputElement).blur();
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ setColumnGap(liveColumnGap() + MARGIN_STEP);
+ } else if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ setColumnGap(liveColumnGap() - MARGIN_STEP);
+ }
+ }
+
// --- Special character insertion (SpecialCharPicker.svelte) ---
let specialCharOpen = $state(false);
@@ -678,6 +762,7 @@
highlightColorOpen = false;
layoutOpen = false;
tableOpen = false;
+ columnsOpen = false;
}
function insertSpecialChar(char: string, range: { from: number; to: number }) {
@@ -1238,6 +1323,95 @@
+
+
+ {#if columnsOpen}
+
+
{t().toolbarExpanded.columns}
+
+
+
+
+
+
{t().toolbarExpanded.columnGap}
+
+
+ {/if}
+