image-gin

Ideogram + Recraft UX parity: master size toggle, last-session persistence, frontmatter auto-init, brand-template copy rewrite

Summary

Follow-up to the initial Ideogram integration (commits e17159c, 8ccf789). The core API wiring works; this round addresses the friction the user hit while actually using it: opaque brand-template fields, no "select all" affordance on size toggles, modal state forgotten between opens, and an invisible image_prompt frontmatter convention that left first-time users with no signal that the plugin uses it.

The same UI affordances were ported to the Recraft modal so both providers feel symmetrical. The two modals are now functionally identical for everything that isn't a genuine API-capability difference.

1. Master "All" size toggle (both modals)

The Image Sizes section header now carries a flush-right "All" toggle alongside the title. Click on → every configured size enables. Click off → all clear. The two-way sync is the interesting bit:

  • Master toggle ON updates individual size toggles via tracked ToggleComponent references.

  • Individual size toggle changes recompute "are all selected?" and call setValue() on the master to reflect.

  • Obsidian's ToggleComponent.setValue() doesn't re-fire onChange (per their convention), so no infinite loops.

State tracked via two new modal fields:

TS
private masterSizeToggle: ToggleComponent | null = null;
private sizeToggles: Map<string, ToggleComponent> = new Map();

Header layout uses inline flex (display: flex; justify-content: space-between) on the existing .image-gin-section-header div — no CSS changes, no new selectors. The Setting component for the master toggle has its .setting-item-info div removed post-render so the toggle hugs the right edge cleanly without the empty info column the Setting constructor inserts by default.

2. Last-session persistence via plugin.saveData() (both modals)

Modal-state fatigue was real — users were re-toggling banner/portrait/square and re-flipping the "Write to frontmatter" toggle on every open. Added a lastSession block to settings, restored on construct, persisted on generate.

Settings shape additions

IdeogramSessionState (six fields — Ideogram has more per-call knobs):

TS
selectedSizes: string[];
styleType: IdeogramStyleType;
renderingSpeed: IdeogramRenderingSpeed;
magicPrompt: IdeogramMagicPrompt;
layerizeText: boolean;
writeToFrontmatter: boolean;

RecraftSessionState (two fields — Recraft has no per-call style overrides; style lives in settings):

TS
selectedSizes: string[];
writeToFrontmatter: boolean;

Both nested into ImageGinSettings. IdeogramSessionState lives at ideogram.lastSession (matches existing nested provider pattern); RecraftSessionState lives at top-level recraftLastSession (matches existing flat recraft* pattern — deliberately avoiding a partial migration into recraft: { ... } per the original blueprint's out-of-scope list).

Hydration discipline

In each modal's constructor, size IDs from the persisted session are filtered against the current settings.imageSizes set before being added to selectedSizes:

TS
const validSizeIds = new Set(this.plugin.settings.imageSizes.map(s => s.id));
for (const id of session.selectedSizes) {
    if (validSizeIds.has(id)) this.selectedSizes.add(id);
}

If a user deletes a size preset from settings, stale IDs are dropped silently rather than haunting the modal.

Persistence timing

Save runs at the start of handleGenerate, before the API call:

TS
this.plugin.settings.ideogram.lastSession = { ...current modal state };
await this.plugin.saveSettings();
try { ... API call ... }

Rationale: even if the API fails, the user's UI choices are remembered. Saving on every individual toggle would be wasteful (write amplification) and would pollute the file with half-baked state if the user just opens the modal and cancels.

What's intentionally NOT persisted

  • imagePrompt — per-file, lives in frontmatter

  • negativePrompt (Ideogram) — derived per-open from settings.baseNegativePrompt + frontmatter.image_negative_prompt; persisting would surprise across files

  • seed (Ideogram) — also per-file, comes from image_seed frontmatter

Mental model is now three-layered: settings tab = brand-wide constants, frontmatter = per-file content, lastSession = "where I left off" UI state.

3. Frontmatter image_prompt auto-creation on modal open (both modals)

Marketplace-readiness fix. Before: a user opening the modal on a fresh note saw an empty textarea with no signal that the plugin uses an image_prompt frontmatter convention. The key only appeared in the file after a successful generate, and only if the "Write to frontmatter" toggle was on. Hidden contract.

After: both modals run this sequence in their open path:

  1. Look up frontmatter[settings.imagePromptKey] via metadata cache.

  2. If undefined (key missing or no frontmatter at all): processFrontMatter writes the key with an empty value. Obsidian's emitter creates the --- block if it doesn't exist.

  3. If present: read it as before.

Inside the processFrontMatter callback, a redundant if (m[key] === undefined) guard prevents a stale-cache race from clobbering a real value with "".

Recraft's loadExistingPrompt was already async, so this was a body change. Ideogram's applyFrontmatterOverrides was sync — converted to async and the onOpen call site now awaits it.

The "Write prompt to frontmatter" toggle still has a use: turn it off to test a one-off prompt without overwriting the saved value. Its scope just narrows from "controls whether the key exists" to "controls whether the textarea's current value is persisted at generate time."

This neutralizes a marketplace-reviewer pushback pattern (hidden conventions / silent no-ops) without removing user control.

4. Brand Template copy rewrite (settings)

The user reported the prefix/suffix fields were confusing — the existing one-line description didn't make the two assembly modes (bookend vs. slot insertion) explicit, and the placeholder examples didn't communicate what each field is for.

Settings → Brand Template intro

Replaced the single inline <p> with a structured explanation: a paragraph defining the per-file prompt, an ordered list with <strong>-tagged mode names ("Bookends" and "Slot insertion"), each with a concrete example and the literal prefix + per-file prompt + suffix formula in <code>. Closing pointer to the modal's Resolved Prompt Preview.

Field-level labels and descriptions

Renamed for clarity:

FieldOld labelNew label
brandTemplate.prefix"Prompt prefix""Prompt prefix — Style Notes"
brandTemplate.suffix"Prompt suffix""Prompt suffix — Brand Alignment"

Each field's description now answers "what is this for?" first, then states the assembly rule. Placeholder examples (provided directly by the user during this session):

  • Prefix: e.g. Style Notes: Comic-book editorial illustration in a clean modern style: {prompt}. Vibrant flat colors, slight halftone texture, confident inked outlines, dynamic composition.

  • Suffix: e.g. Brand Alignment: Include colors {list colors and hex values}, with green and blue being more background ambient colors to keep the feel aligned with brand

  • Base negative: e.g. no text, no watermarks, no signatures, no captions, no stock-photo aesthetic

The user's mental division (Style Notes for visual approach, Brand Alignment for hard color/motif constraints, Base negative for never-allowed content) is now embedded in the UI rather than something a reader has to infer.

5. Ideogram modal copy rewrite

To match the new mental model from §4:

  • Image Prompt section header: "Image Prompt (subject matter)" → "Image Prompt — Subject Matter"

  • Above the prompt textarea: a help paragraph saying explicitly describe ONLY scene content; style and brand colors come from settings. Distinguishes the per-file slot from the brand-wide template at the point of use.

  • Image prompt placeholder: replaced the short example with a fully worked one (the user's own AI-Agents scene) so the field's intent is unmistakable.

  • Resolved Prompt Preview section: now shows mode-aware guidance text above the preview block:

    • No prefix/suffix configured → "No brand template configured — your prompt is sent to Ideogram exactly as typed. Set a prefix/suffix in plugin settings to wrap it automatically."

    • {prompt} token in prefix → "Slot-insertion mode: your prompt is substituted into the prefix at the {prompt} token; suffix is ignored."

    • Otherwise → "Bookend mode: prefix prepended, suffix appended (separated by spaces)."

    The "no template configured" hint is the most useful — when both prefix and suffix are empty (the user's reported confusion), the preview previously just echoed the typed prompt with no explanation; now the modal tells the reader why.

  • Negative prompt override label and placeholder: relabeled "Negative prompt — what to exclude from this image" with a placeholder spelling out that the field is pre-merged from settings + frontmatter and is editable for one-off run additions.

6. Diagnostic logging in the size-toggle path (both modals)

While debugging the user's "I toggled banner but got back all three" report, added structured [Ideogram] / [Recraft] -prefixed log entries on every size toggle change and at the start of handleGenerate. Each log line shows which IDs are in selectedSizes post-mutation and (in handleGenerate) which IDs the loop will iterate.

Recraft's CurrentFileModal already had similar logging from a prior session — kept as-is and brought Ideogram up to the same diagnostic level. The user's bug turned out not to be in the toggle code (selection state was correct); the logging stays as load-bearing diagnostics for any future regressions in this exact failure mode.

7. Version bump to 0.1.1

Three files synced (same pattern as the prior 0.0.9 → 0.1.0 bump in 2026-05-03_01.md §5):

  • manifest.json version: 0.1.00.1.1

  • package.json version: 0.1.00.1.1

  • versions.json: added "0.1.1": "1.8.10" (kept 0.0.9 and 0.1.0 entries)

minAppVersion stayed at 1.8.10 — none of this session's work uses APIs newer than that. Bumped manually rather than via pnpm version to keep this commit's file list scoped to what actually changed.

8. Build artifacts

styles.css regenerated by pnpm build — the bundle's content is identical in intent (same source CSS in src/styles/); the diff is non-semantic esbuild output churn. Re-bundled as part of verifying every change in this session.

log.json is the runtime FileLogger output (created during testing). Appears modified because actual API requests + responses were logged during the session's manual test runs. Not a source change.

API parity matrix (after this session)

FeatureRecraftIdeogramNotes
image_prompt auto-create on modal openNew, both — §3
Master "All" toggle in size headerNew, both — §1
Two-way master ↔ individual syncNew, both — §1
lastSession UI-state persistenceNew, both — §2
Per-call style overridesIdeogram-only API capability (style_type/magic_prompt/rendering_speed are per-request)
Brand-template prompt wrappingIdeogram-only (Recraft uses server-side style_id for the equivalent)
Resolved Prompt PreviewIdeogram-only (only meaningful when wrapping happens)
Layerize-text post-processIdeogram-only API endpoint
Per-file frontmatter overrides (image_negative_prompt / image_style_type / image_seed)Ideogram-only
Custom style_id / imageStylesJSONRecraft-only API capability

Asymmetries that remain are genuine API-capability differences, not UX inconsistencies.

Verified working

  • pnpm build — green: ESLint passes (zero warnings), tsc -noEmit -skipLibCheck passes, esbuild produces main.js + styles.css.

  • Manual smoke test of Ideogram modal: master toggle flips all three size toggles; individual toggle change flips master to OFF; close + reopen restores selection; close + reopen on a no-frontmatter note creates image_prompt: "" block; per-call override dropdowns persist across opens.

  • Recraft modal: same master-toggle behavior; close + reopen restores selection and writeToFrontmatter choice.

Files touched

Modified:

  • src/settings/settings.tsIdeogramSessionState + RecraftSessionState types, lastSession defaults nested under ideogram, recraftLastSession at top level, Brand Template intro paragraph rewrite + field-level renames + placeholder updates

  • src/modals/IdeogramModal.tsToggleComponent ref tracking, master toggle in size header with two-way sync, lastSession hydrate-on-construct + save-on-generate, applyFrontmatterOverrides made async with auto-create of missing image_prompt key, prompt section + preview section copy updates, negative-prompt textarea label + placeholder

  • src/modals/CurrentFileModal.ts — same patterns: ToggleComponent ref tracking, master size toggle, recraftLastSession hydrate + save, loadExistingPrompt updated to auto-create missing image_prompt

Build/runtime artifacts (not source changes):

  • styles.css — esbuild re-bundle (same source CSS)

  • log.json — FileLogger output from manual testing