Foundation for the compose-editor rich formatting epic (strikethrough, font family/size/color, highlight, paragraph alignment, inline images, remembered font defaults). Today RichStyle is a parameterless enum (BOLD/ITALIC/UNDERLINE), so none of those features fit the model. All other epic tickets stack on this branch: feat-richtext-foundation.
RichTextContent gains alignments: List<RichAlignment> (RichAlign START/CENTER/END, paragraph ranges), images: List<RichImage> (start, end, contentId, name — the range covers a visible [image: name] token in the plain text), and baseStyle: RichBaseStyle? (fontCss, fontSizePt).
Serializer: parameterized styles as merged <span style="…">; alignment as text-align on <p>/<li> (consecutive-line <p> merging must split at alignment boundaries); images as <img src="cid:…" alt="name"> replacing the token; baseStyle as a single outer <div style="…"> (no stray newlines).
Parser: faithful inverse of everything above; also <s>/<del>/<strike>, strong/em, pt and px sizes, hex colors; unknown CSS properties ignored without dropping the text run.
hasFormatting() covers every new channel (any span, link, block marker, alignment, image, or baseStyle).
Editor mapping (ui/compose/RichTextEditor.kt): style identity carried through the AnnotatedString via string annotations (like the existing URL_TAG) so FontColor can never be confused with link paint; visual SpanStyle derived per style; toAnnotatedString gains a font-resolver hook (css → Compose FontFamily, default = system fonts); baseStyle applied to the field's text style; alignment ranges → ParagraphStyle(textAlign). RichTextBodyField's signature stays source-compatible (new params defaulted — it is also used by SignatureEditScreen).
RichTextEditing.toggleStyle/isStyled generalized: toggling the exact style off; applying a different value of the same kind replaces it. (Alignment/image ops are NOT in scope — later tickets own them.)
New reusable ui/compose/format/ColorSwatchRow.kt.
Acceptance
Zero visible UI change; all existing tests stay green.
Exhaustive round-trip tests: for every new style/channel, fromHtml(toHtml(x)) == xandhasFormatting() is true — this guards ComposeViewModel.normalizedHtml(), which silently drops any HTML the parser cannot read back as formatted.
Foundation for the compose-editor rich formatting epic (strikethrough, font family/size/color, highlight, paragraph alignment, inline images, remembered font defaults). Today `RichStyle` is a parameterless enum (`BOLD/ITALIC/UNDERLINE`), so none of those features fit the model. All other epic tickets stack on this branch: `feat-richtext-foundation`.
**Scope** (`org.libremail.richtext`, pure JVM):
- `RichStyle` becomes a sealed interface: `Bold`, `Italic`, `Underline`, `Strikethrough`, `FontFamily(css: String)`, `FontSize(pt: Int)`, `FontColor(argb: Int)`, `Highlight(argb: Int)`.
- `RichTextContent` gains `alignments: List<RichAlignment>` (`RichAlign START/CENTER/END`, paragraph ranges), `images: List<RichImage>` (`start, end, contentId, name` — the range covers a visible `[image: name]` token in the plain text), and `baseStyle: RichBaseStyle?` (`fontCss`, `fontSizePt`).
- Serializer: parameterized styles as merged `<span style="…">`; alignment as `text-align` on `<p>/<li>` (consecutive-line `<p>` merging must split at alignment boundaries); images as `<img src="cid:…" alt="name">` replacing the token; baseStyle as a single outer `<div style="…">` (no stray newlines).
- Parser: faithful inverse of everything above; also `<s>/<del>/<strike>`, `strong/em`, `pt` and `px` sizes, hex colors; unknown CSS properties ignored without dropping the text run.
- `hasFormatting()` covers every new channel (any span, link, block marker, alignment, image, or baseStyle).
- Editor mapping (`ui/compose/RichTextEditor.kt`): style identity carried through the AnnotatedString via string annotations (like the existing `URL_TAG`) so `FontColor` can never be confused with link paint; visual `SpanStyle` derived per style; `toAnnotatedString` gains a font-resolver hook (css → Compose `FontFamily`, default = system fonts); baseStyle applied to the field's text style; alignment ranges → `ParagraphStyle(textAlign)`. `RichTextBodyField`'s signature stays source-compatible (new params defaulted — it is also used by `SignatureEditScreen`).
- `RichTextEditing.toggleStyle/isStyled` generalized: toggling the exact style off; applying a different value of the same kind replaces it. (Alignment/image *ops* are NOT in scope — later tickets own them.)
- New reusable `ui/compose/format/ColorSwatchRow.kt`.
**Acceptance**
- Zero visible UI change; all existing tests stay green.
- Exhaustive round-trip tests: for every new style/channel, `fromHtml(toHtml(x)) == x` **and** `hasFormatting()` is true — this guards `ComposeViewModel.normalizedHtml()`, which silently drops any HTML the parser cannot read back as formatted.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Foundation for the compose-editor rich formatting epic (strikethrough, font family/size/color, highlight, paragraph alignment, inline images, remembered font defaults). Today
RichStyleis a parameterless enum (BOLD/ITALIC/UNDERLINE), so none of those features fit the model. All other epic tickets stack on this branch:feat-richtext-foundation.Scope (
org.libremail.richtext, pure JVM):RichStylebecomes a sealed interface:Bold,Italic,Underline,Strikethrough,FontFamily(css: String),FontSize(pt: Int),FontColor(argb: Int),Highlight(argb: Int).RichTextContentgainsalignments: List<RichAlignment>(RichAlign START/CENTER/END, paragraph ranges),images: List<RichImage>(start, end, contentId, name— the range covers a visible[image: name]token in the plain text), andbaseStyle: RichBaseStyle?(fontCss,fontSizePt).<span style="…">; alignment astext-alignon<p>/<li>(consecutive-line<p>merging must split at alignment boundaries); images as<img src="cid:…" alt="name">replacing the token; baseStyle as a single outer<div style="…">(no stray newlines).<s>/<del>/<strike>,strong/em,ptandpxsizes, hex colors; unknown CSS properties ignored without dropping the text run.hasFormatting()covers every new channel (any span, link, block marker, alignment, image, or baseStyle).ui/compose/RichTextEditor.kt): style identity carried through the AnnotatedString via string annotations (like the existingURL_TAG) soFontColorcan never be confused with link paint; visualSpanStylederived per style;toAnnotatedStringgains a font-resolver hook (css → ComposeFontFamily, default = system fonts); baseStyle applied to the field's text style; alignment ranges →ParagraphStyle(textAlign).RichTextBodyField's signature stays source-compatible (new params defaulted — it is also used bySignatureEditScreen).RichTextEditing.toggleStyle/isStyledgeneralized: toggling the exact style off; applying a different value of the same kind replaces it. (Alignment/image ops are NOT in scope — later tickets own them.)ui/compose/format/ColorSwatchRow.kt.Acceptance
fromHtml(toHtml(x)) == xandhasFormatting()is true — this guardsComposeViewModel.normalizedHtml(), which silently drops any HTML the parser cannot read back as formatted.