From 265e3de0af33f4a50e73d464d3c7016a17ba7404 Mon Sep 17 00:00:00 2001 From: dani-polani Date: Sat, 8 Aug 2026 02:11:37 +0300 Subject: [PATCH 1/5] feat(layout): vertical writing systems (tategaki column mode) Splits two things that were fused into one assumption ("lines stack down, tokens flow across, connectors run vertically"): - `layoutAxis` (project-wide): `rows` as before, or `columns` where every line becomes a vertical column and connectors run sideways. Plus `columnOrder` for which edge the first line sits on (right for CJK, left for Mongolian). The axis is project-wide on purpose: a connector between an adjacent pair needs one consistent "between" direction, and a line pair gap is a single number. - `textOrientation` (per line): `upright` keeps words horizontal, `vertical` stacks characters (tategaki), `sideways` rotates the line. So a Japanese column stacks glyphs while the English column next to it stays a stack of upright word boxes. Column mode sets the token row itself vertically, so flex direction and the logical margin between tokens map to the block axis and DOM measurement is unchanged. Link geometry, auto-fit, and `refitLayoutToFill` take the axis; SVG export draws vertical tokens as one tspan per character (SVG 1.1 writing-mode is not honored by the PNG/PDF rasterizer) and sideways tokens as a rotation about the box center. Compact v4 gains two settings keys and one trailing line column, both omitted at their defaults, so existing share links decode unchanged. Not included, tracked as follow-ups: API/MCP/OpenAPI surface, vertical HarfBuzz shaping for custom-font path export (CJK brackets and punctuation need `vert`/`vrt2` and vertical advances), automatic CJK segmentation. Co-Authored-By: Claude Opus 5 (1M context) --- bitext/src/app.css | 58 ++++++ .../components/editor/LineSettingsForm.svelte | 16 ++ .../preview/AlignmentPreview.svelte | 43 ++++- .../components/preview/AlignmentSvg.svelte | 42 ++++- .../preview/LinePairGapSlider.svelte | 70 +++++-- .../preview/LineReorderButtons.svelte | 36 +++- .../lib/components/preview/TokenRow.svelte | 27 ++- .../lib/components/preview/TokenView.svelte | 13 +- .../lib/components/share/ExportMenu.svelte | 4 +- .../components/shell/panels/StylePanel.svelte | 45 ++++- bitext/src/lib/domain/autofit.test.ts | 26 +-- bitext/src/lib/domain/autofit.ts | 15 +- bitext/src/lib/domain/link-geometry.test.ts | 79 ++++++++ bitext/src/lib/domain/link-geometry.ts | 49 ++++- bitext/src/lib/examples/data/core.ts | 24 +++ bitext/src/lib/examples/data/index.ts | 9 +- bitext/src/lib/export/svg-vertical.test.ts | 175 +++++++++++++++++ bitext/src/lib/export/svg.ts | 178 +++++++++++++----- bitext/src/lib/serialization/compact-v4.ts | 22 ++- bitext/src/lib/serialization/schema.ts | 40 +++- .../vertical-writing-roundtrip.test.ts | 103 ++++++++++ bitext/src/lib/state/project.svelte.ts | 8 +- bitext/src/lib/types/layout.ts | 23 +++ 23 files changed, 972 insertions(+), 133 deletions(-) create mode 100644 bitext/src/lib/domain/link-geometry.test.ts create mode 100644 bitext/src/lib/export/svg-vertical.test.ts create mode 100644 bitext/src/lib/serialization/vertical-writing-roundtrip.test.ts diff --git a/bitext/src/app.css b/bitext/src/app.css index d3aa592..77c837c 100644 --- a/bitext/src/app.css +++ b/bitext/src/app.css @@ -244,6 +244,41 @@ html { align-items: stretch; } +/* Column mode: lines sit side by side. `row-reverse` puts the first line rightmost (CJK); + the interleaved gap strips reverse with them, so pair order stays correct either way. */ +.preview-stack--columns { + flex-direction: row; + align-items: stretch; + justify-content: center; + height: 100%; +} + +.preview-stack--columns-rtl { + flex-direction: row-reverse; +} + +/* Column mode grows sideways, so the block axis needs a bound for auto-fit to target and for a + long sentence to shrink instead of running off the canvas. */ +.preview-frame[data-aligner-axis='columns'] { + height: min(70vh, 44rem); +} + +.preview-frame[data-aligner-axis='columns'] .preview-zoom { + height: 100%; +} + +.preview-stack--columns .preview-token-line { + min-height: 0; + height: 100%; +} + +.preview-stack--columns .preview-gloss-wrap { + width: auto; + height: 100%; + min-height: 0; + align-items: center; +} + .preview-frame__attribution { position: relative; z-index: 3; @@ -378,6 +413,29 @@ path.link-path { flex-wrap: wrap; } +/* Column mode: setting the row itself vertically makes flex direction and the logical + `margin-inline-start` between tokens map to the block axis with no extra branching. + Baseline alignment is dropped because a column may mix upright and vertically set tokens. */ +.token-row--columns { + writing-mode: vertical-rl; + align-items: center; +} + +/* Glyph setting per line. `upright` is explicit so it resets the inherited vertical row above. */ +.token-view--upright { + writing-mode: horizontal-tb; +} + +.token-view--vertical { + writing-mode: vertical-rl; + text-orientation: upright; +} + +.token-view--sideways { + writing-mode: vertical-rl; + text-orientation: sideways; +} + .token-view { display: inline-flex; align-items: baseline; diff --git a/bitext/src/lib/components/editor/LineSettingsForm.svelte b/bitext/src/lib/components/editor/LineSettingsForm.svelte index 016a079..4285452 100644 --- a/bitext/src/lib/components/editor/LineSettingsForm.svelte +++ b/bitext/src/lib/components/editor/LineSettingsForm.svelte @@ -3,6 +3,7 @@ import { TrashBinOutline } from 'flowbite-svelte-icons'; import { Label, Range } from 'flowbite-svelte'; import type { LineV2 } from '$lib/serialization/schema.js'; + import type { TextOrientation } from '$lib/types/layout.js'; import { GOOGLE_FONT_OPTIONS } from '$lib/fonts/google-fonts.js'; import { MAX_TEXT_SIZE_PX, @@ -258,6 +259,21 @@ /> Right-to-left row +
+ + +
{#if nextLine}