fix: update preview spacing.
689e9c163b82111544b10ab3a237b6c774aff4f0
Kevin Kortum <kevinkortum@me.com> · 2026-06-30 11:11
parent 9732e1f8
4 files changed
- → JPG-export-text-to-4-5-social-image.md +25 −0
@@ -0,0 +1,25 @@
+---
+id: TASK-031
+title: 'JPG export: text to 4:5 social image'
+status: "\U0001F7E6 Backlog"
+assignee: []
+created_date: '2026-06-30 18:04'
+labels:
+ - feature
+dependencies: []
+priority: medium
+ordinal: 30000
+---
+
+## Description
+
+<!-- SECTION:DESCRIPTION:BEGIN -->
+Render the current buffer's text smartly laid out into a 4:5 (1080x1350) tall JPG for social posting. Sibling to the HTML export (-e). Open Qs: auto font-size/scale to fit, multi-image overflow when text too long, theme colors reuse from preview/export, CLI flag (e.g. -j/--jpg). Smart layout: margins, line wrap, vertical centering/balance.
+<!-- SECTION:DESCRIPTION:END -->
+
+## Acceptance Criteria
+<!-- AC:BEGIN -->
+- [ ] #1 A buffer exports to a 1080x1350 (4:5) JPG via a CLI flag
+- [ ] #2 Text is laid out legibly with sensible margins and wrapping
+- [ ] #3 Theme colors match preview/export house style
+<!-- AC:END -->
internal/export/assets/doc.css +2 −2
@@ -146,10 +146,10 @@ .doc h1, .doc h2, .doc h3, .doc h4, .doc h5, .doc h6 {
font-family: var(--font-display);
line-height: var(--leading-tight);
color: var(--text);
- margin: 2.4em 0 0.6em;
+ margin: 1.2em 0 0;
text-wrap: balance;
}
-.doc h1 { font-size: 3rem; font-weight: 700; letter-spacing: -0.01em; margin-top: 0; }
+.doc h1 { font-size: 3rem; font-weight: 700; letter-spacing: -0.01em; margin-top: 0; margin-bottom: 0.6em; }
.doc h2 { font-size: 2.2rem; font-weight: 700; letter-spacing: -0.005em; }
.doc h3 { font-size: 1.6rem; font-weight: 400; }
.doc h4 { font-size: 1.18rem; font-weight: 700; font-family: var(--font-body); }
internal/preview/preview.go +7 −0
@@ -62,6 +62,13 @@ ht := legibleText(heading)
cfg.H1.Color = &ht
cfg.H1.BackgroundColor = &heading
cfg.H1.Bold = &yes
+
+ // Drop the blank line glamour emits after every heading (its BlockSuffix
+ // "\n") for H2-H6, so a subhead sits directly above its body. H1 keeps the
+ // gap: empty the shared Heading suffix (which H2-H6 inherit) and re-assert it
+ // on H1 only. Cascade overrides parent with child only when child is non-empty.
+ cfg.Heading.BlockSuffix = ""
+ cfg.H1.BlockSuffix = "\n"
}
// clearAllBackgrounds nils every *string field named "BackgroundColor" in the
internal/preview/preview_test.go +38 −0
@@ -2,6 +2,7 @@ package preview
import (
"reflect"
+ "regexp"
"strings"
"testing"
@@ -60,3 +61,40 @@ if cfg.H1.Color == nil || *cfg.H1.Color != "#FFFCF0" {
t.Errorf("H1 text not legible color, got %v", cfg.H1.Color)
}
}
+
+// Headings below H1 should sit directly above their body — no blank line.
+func TestRenderNoBlankLineAfterSubheadings(t *testing.T) {
+ m := New("")
+ m.SetColors(Colors{Background: "#100F0F", Text: "#CECDC3", Heading: "#4385BE", Code: "#879A39", Link: "#3AA99F"})
+ m.SetSize(80, 40)
+ if err := m.Render("# Title\n\nintro\n\n## Section\n\nbody\n\n### Sub\n\nmore\n"); err != nil {
+ t.Fatal(err)
+ }
+ lines := stripANSI(m.vp.View())
+ idx := func(want string) int {
+ for i, ln := range lines {
+ if strings.TrimSpace(ln) == want {
+ return i
+ }
+ }
+ t.Fatalf("line %q not found in:\n%s", want, strings.Join(lines, "\n"))
+ return -1
+ }
+ // H2/H3 are immediately followed by their body — no intervening blank line.
+ if got := strings.TrimSpace(lines[idx("## Section")+1]); got != "body" {
+ t.Errorf("expected body directly after H2, got %q", got)
+ }
+ if got := strings.TrimSpace(lines[idx("### Sub")+1]); got != "more" {
+ t.Errorf("expected body directly after H3, got %q", got)
+ }
+ // H1 keeps its trailing blank line.
+ if got := strings.TrimSpace(lines[idx("Title")+1]); got != "" {
+ t.Errorf("expected blank line after H1, got %q", got)
+ }
+}
+
+var ansiRE = regexp.MustCompile("\x1b\\[[0-9;]*m")
+
+func stripANSI(s string) []string {
+ return strings.Split(ansiRE.ReplaceAllString(s, ""), "\n")
+}