▍ humdrum codex / glint v1.0.2
license AGPL-3.0

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")
+}