feat: add live preview pane and status bar to generator modal

Collapsible preview shows first 30 lines of generated output with
real-time updates (300ms debounce). Status bar displays file count,
token estimate, active target, and output format — all updating as
options change. Token count warns when exceeding target limit.
Clicking target scrolls to config, clicking format cycles options.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca G. Oelfke 2026-02-06 11:39:13 +01:00
parent 08b8a180ca
commit 2f3574c63f
No known key found for this signature in database
GPG key ID: E22BABF67200F864
3 changed files with 304 additions and 17 deletions

View file

@ -2,7 +2,7 @@ import { App, Modal, Notice, Setting, TFile, TFolder } from 'obsidian';
import ClaudeContextPlugin from './main'; import ClaudeContextPlugin from './main';
import { createFreetextSource, SourcePosition } from './sources'; import { createFreetextSource, SourcePosition } from './sources';
import { estimateTokens } from './history'; import { estimateTokens } from './history';
import { OutputTarget, getTargetIcon, formatTokenCount } from './targets'; import { OutputTarget, OutputFormat, getTargetIcon, formatTokenCount } from './targets';
interface FolderConfig { interface FolderConfig {
name: string; name: string;
@ -87,10 +87,26 @@ export class ContextGeneratorModal extends Modal {
selectedTemplateId: string | null = null; selectedTemplateId: string | null = null;
selectedTargetIds: Set<string> = new Set(); selectedTargetIds: Set<string> = new Set();
// Preview pane state
private previewContent: HTMLElement | null = null;
private previewLineInfo: HTMLElement | null = null;
private debounceTimer: ReturnType<typeof setTimeout> | null = null;
private previewOpen: boolean = false;
private static readonly PREVIEW_LINES = 30;
private static readonly DEBOUNCE_MS = 300;
// Status bar DOM references
private statusFileCount: HTMLElement | null = null;
private statusTokens: HTMLElement | null = null;
private statusTarget: HTMLElement | null = null;
private statusFormat: HTMLElement | null = null;
private targetsHeadingEl: HTMLElement | null = null;
constructor(app: App, plugin: ClaudeContextPlugin) { constructor(app: App, plugin: ClaudeContextPlugin) {
super(app); super(app);
this.plugin = plugin; this.plugin = plugin;
this.config = JSON.parse(JSON.stringify(DEFAULT_CONFIG)); this.config = JSON.parse(JSON.stringify(DEFAULT_CONFIG));
this.previewOpen = this.plugin.settings.generatorPreviewOpen ?? false;
this.config.folders = this.scanVaultStructure().map(name => ({ name, purpose: '' })); this.config.folders = this.scanVaultStructure().map(name => ({ name, purpose: '' }));
// Initialize with default template // Initialize with default template
this.selectedTemplateId = this.plugin.settings.defaultTemplateId; this.selectedTemplateId = this.plugin.settings.defaultTemplateId;
@ -108,6 +124,39 @@ export class ContextGeneratorModal extends Modal {
contentEl.createEl('h2', { text: 'Context Generator' }); contentEl.createEl('h2', { text: 'Context Generator' });
// === STATUS BAR ===
const statusBar = contentEl.createDiv({ cls: 'cc-gen-statusbar' });
this.statusFileCount = statusBar.createEl('span', { cls: 'cc-statusbar-item' });
statusBar.createEl('span', { cls: 'cc-statusbar-sep', text: '\u00B7' });
this.statusTokens = statusBar.createEl('span', { cls: 'cc-statusbar-item cc-statusbar-tokens' });
statusBar.createEl('span', { cls: 'cc-statusbar-sep', text: '\u00B7' });
this.statusTarget = statusBar.createEl('span', { cls: 'cc-statusbar-item cc-statusbar-clickable' });
statusBar.createEl('span', { cls: 'cc-statusbar-sep', text: '\u00B7' });
this.statusFormat = statusBar.createEl('span', { cls: 'cc-statusbar-item cc-statusbar-clickable' });
// Click target label → scroll to Output Targets section
this.statusTarget.addEventListener('click', () => {
if (this.targetsHeadingEl) {
this.targetsHeadingEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
// Click format label → cycle through formats on primary target
this.statusFormat.addEventListener('click', () => {
const primary = this.getPrimaryTarget();
if (!primary) return;
const formats: OutputFormat[] = ['markdown', 'xml', 'plain'];
const idx = formats.indexOf(primary.format);
primary.format = formats[(idx + 1) % formats.length] ?? 'markdown';
this.plugin.saveSettings();
this.updateStatusBar();
this.schedulePreviewUpdate();
});
// Initial status bar render
this.updateStatusBar();
// === TWO-ZONE GRID LAYOUT === // === TWO-ZONE GRID LAYOUT ===
const layout = contentEl.createDiv({ cls: 'cc-gen-layout' }); const layout = contentEl.createDiv({ cls: 'cc-gen-layout' });
const selectionZone = layout.createDiv({ cls: 'cc-gen-zone cc-gen-selection' }); const selectionZone = layout.createDiv({ cls: 'cc-gen-zone cc-gen-selection' });
@ -124,31 +173,31 @@ export class ContextGeneratorModal extends Modal {
.setName('conventions.md') .setName('conventions.md')
.addToggle(toggle => toggle .addToggle(toggle => toggle
.setValue(this.config.generateFiles.conventions) .setValue(this.config.generateFiles.conventions)
.onChange(v => this.config.generateFiles.conventions = v)); .onChange(v => { this.config.generateFiles.conventions = v; this.schedulePreviewUpdate(); }));
new Setting(selectionZone) new Setting(selectionZone)
.setName('structure.md') .setName('structure.md')
.addToggle(toggle => toggle .addToggle(toggle => toggle
.setValue(this.config.generateFiles.structure) .setValue(this.config.generateFiles.structure)
.onChange(v => this.config.generateFiles.structure = v)); .onChange(v => { this.config.generateFiles.structure = v; this.schedulePreviewUpdate(); }));
new Setting(selectionZone) new Setting(selectionZone)
.setName('workflows.md') .setName('workflows.md')
.addToggle(toggle => toggle .addToggle(toggle => toggle
.setValue(this.config.generateFiles.workflows) .setValue(this.config.generateFiles.workflows)
.onChange(v => this.config.generateFiles.workflows = v)); .onChange(v => { this.config.generateFiles.workflows = v; this.schedulePreviewUpdate(); }));
new Setting(selectionZone) new Setting(selectionZone)
.setName('templates.md') .setName('templates.md')
.addToggle(toggle => toggle .addToggle(toggle => toggle
.setValue(this.config.generateFiles.templates) .setValue(this.config.generateFiles.templates)
.onChange(v => this.config.generateFiles.templates = v)); .onChange(v => { this.config.generateFiles.templates = v; this.schedulePreviewUpdate(); }));
new Setting(selectionZone) new Setting(selectionZone)
.setName('examples.md') .setName('examples.md')
.addToggle(toggle => toggle .addToggle(toggle => toggle
.setValue(this.config.generateFiles.examples) .setValue(this.config.generateFiles.examples)
.onChange(v => this.config.generateFiles.examples = v)); .onChange(v => { this.config.generateFiles.examples = v; this.schedulePreviewUpdate(); }));
// === FOLDER STRUCTURE === // === FOLDER STRUCTURE ===
selectionZone.createEl('h3', { text: 'Folder structure' }); selectionZone.createEl('h3', { text: 'Folder structure' });
@ -170,7 +219,7 @@ export class ContextGeneratorModal extends Modal {
.addOption('flat', 'Flat (#tag)') .addOption('flat', 'Flat (#tag)')
.addOption('none', 'No tags') .addOption('none', 'No tags')
.setValue(this.config.tagsStyle) .setValue(this.config.tagsStyle)
.onChange(v => this.config.tagsStyle = v)); .onChange(v => { this.config.tagsStyle = v; this.schedulePreviewUpdate(); }));
new Setting(selectionZone) new Setting(selectionZone)
.setName('Predefined tags') .setName('Predefined tags')
@ -180,6 +229,7 @@ export class ContextGeneratorModal extends Modal {
.setValue(this.config.customTags.join(', ')) .setValue(this.config.customTags.join(', '))
.onChange(v => { .onChange(v => {
this.config.customTags = v.split(',').map(s => s.trim()).filter(s => s); this.config.customTags = v.split(',').map(s => s.trim()).filter(s => s);
this.schedulePreviewUpdate();
})); }));
// === FRONTMATTER SECTION === // === FRONTMATTER SECTION ===
@ -192,6 +242,7 @@ export class ContextGeneratorModal extends Modal {
.setValue(this.config.frontmatterFields.join(', ')) .setValue(this.config.frontmatterFields.join(', '))
.onChange(v => { .onChange(v => {
this.config.frontmatterFields = v.split(',').map(s => s.trim()).filter(s => s); this.config.frontmatterFields = v.split(',').map(s => s.trim()).filter(s => s);
this.schedulePreviewUpdate();
})); }));
// === NOTE TEMPLATES === // === NOTE TEMPLATES ===
@ -229,7 +280,7 @@ export class ContextGeneratorModal extends Modal {
.addTextArea(text => { .addTextArea(text => {
text.setPlaceholder('e.g. Personal Zettelkasten for development and knowledge management') text.setPlaceholder('e.g. Personal Zettelkasten for development and knowledge management')
.setValue(this.config.vaultDescription) .setValue(this.config.vaultDescription)
.onChange(v => this.config.vaultDescription = v); .onChange(v => { this.config.vaultDescription = v; this.schedulePreviewUpdate(); });
text.inputEl.rows = 2; text.inputEl.rows = 2;
text.inputEl.style.width = '100%'; text.inputEl.style.width = '100%';
}); });
@ -240,7 +291,7 @@ export class ContextGeneratorModal extends Modal {
.addOption('english', 'English') .addOption('english', 'English')
.addOption('german', 'Deutsch') .addOption('german', 'Deutsch')
.setValue(this.config.language) .setValue(this.config.language)
.onChange(v => this.config.language = v)); .onChange(v => { this.config.language = v; this.schedulePreviewUpdate(); }));
// === FORMATTING === // === FORMATTING ===
configZone.createEl('h3', { text: 'Formatting' }); configZone.createEl('h3', { text: 'Formatting' });
@ -253,7 +304,7 @@ export class ContextGeneratorModal extends Modal {
.addOption('camelCase', 'camelCase') .addOption('camelCase', 'camelCase')
.addOption('free', 'Free / no convention') .addOption('free', 'Free / no convention')
.setValue(this.config.fileNaming) .setValue(this.config.fileNaming)
.onChange(v => this.config.fileNaming = v)); .onChange(v => { this.config.fileNaming = v; this.schedulePreviewUpdate(); }));
new Setting(configZone) new Setting(configZone)
.setName('Link style') .setName('Link style')
@ -261,7 +312,7 @@ export class ContextGeneratorModal extends Modal {
.addOption('wikilinks', '[[Wikilinks]]') .addOption('wikilinks', '[[Wikilinks]]')
.addOption('markdown', '[Markdown](links)') .addOption('markdown', '[Markdown](links)')
.setValue(this.config.linkStyle) .setValue(this.config.linkStyle)
.onChange(v => this.config.linkStyle = v)); .onChange(v => { this.config.linkStyle = v; this.schedulePreviewUpdate(); }));
new Setting(configZone) new Setting(configZone)
.setName('Heading depth') .setName('Heading depth')
@ -271,7 +322,7 @@ export class ContextGeneratorModal extends Modal {
.addOption('h4', 'H1 - H4') .addOption('h4', 'H1 - H4')
.addOption('h6', 'Unlimited') .addOption('h6', 'Unlimited')
.setValue(this.config.headingDepth) .setValue(this.config.headingDepth)
.onChange(v => this.config.headingDepth = v)); .onChange(v => { this.config.headingDepth = v; this.schedulePreviewUpdate(); }));
new Setting(configZone) new Setting(configZone)
.setName('Date format') .setName('Date format')
@ -280,7 +331,7 @@ export class ContextGeneratorModal extends Modal {
.addOption('DD.MM.YYYY', 'DD.MM.YYYY') .addOption('DD.MM.YYYY', 'DD.MM.YYYY')
.addOption('MM/DD/YYYY', 'MM/DD/YYYY') .addOption('MM/DD/YYYY', 'MM/DD/YYYY')
.setValue(this.config.dateFormat) .setValue(this.config.dateFormat)
.onChange(v => this.config.dateFormat = v)); .onChange(v => { this.config.dateFormat = v; this.schedulePreviewUpdate(); }));
// === RULES === // === RULES ===
configZone.createEl('h3', { text: 'Rules' }); configZone.createEl('h3', { text: 'Rules' });
@ -293,6 +344,7 @@ export class ContextGeneratorModal extends Modal {
.setValue(this.config.customRules.join('\n')) .setValue(this.config.customRules.join('\n'))
.onChange(v => { .onChange(v => {
this.config.customRules = v.split('\n').map(s => s.trim()).filter(s => s); this.config.customRules = v.split('\n').map(s => s.trim()).filter(s => s);
this.schedulePreviewUpdate();
}); });
text.inputEl.rows = 3; text.inputEl.rows = 3;
text.inputEl.style.width = '100%'; text.inputEl.style.width = '100%';
@ -305,6 +357,7 @@ export class ContextGeneratorModal extends Modal {
.setValue(this.config.forbiddenActions.join(', ')) .setValue(this.config.forbiddenActions.join(', '))
.onChange(v => { .onChange(v => {
this.config.forbiddenActions = v.split(',').map(s => s.trim()).filter(s => s); this.config.forbiddenActions = v.split(',').map(s => s.trim()).filter(s => s);
this.schedulePreviewUpdate();
})); }));
// === ADDITIONAL CONTEXT === // === ADDITIONAL CONTEXT ===
@ -316,7 +369,7 @@ export class ContextGeneratorModal extends Modal {
.addTextArea(text => { .addTextArea(text => {
text.setPlaceholder('Enter additional context that will be included when copying...') text.setPlaceholder('Enter additional context that will be included when copying...')
.setValue(this.temporaryFreetext) .setValue(this.temporaryFreetext)
.onChange(v => this.temporaryFreetext = v); .onChange(v => { this.temporaryFreetext = v; this.schedulePreviewUpdate(); });
text.inputEl.rows = 4; text.inputEl.rows = 4;
text.inputEl.style.width = '100%'; text.inputEl.style.width = '100%';
}); });
@ -327,7 +380,7 @@ export class ContextGeneratorModal extends Modal {
.addOption('prefix', 'Prefix (before vault content)') .addOption('prefix', 'Prefix (before vault content)')
.addOption('suffix', 'Suffix (after vault content)') .addOption('suffix', 'Suffix (after vault content)')
.setValue(this.temporaryPosition) .setValue(this.temporaryPosition)
.onChange(v => this.temporaryPosition = v as SourcePosition)); .onChange(v => { this.temporaryPosition = v as SourcePosition; this.schedulePreviewUpdate(); }));
new Setting(configZone) new Setting(configZone)
.setName('Save as default') .setName('Save as default')
@ -357,6 +410,7 @@ export class ContextGeneratorModal extends Modal {
dropdown.setValue(this.selectedTemplateId || ''); dropdown.setValue(this.selectedTemplateId || '');
dropdown.onChange(v => { dropdown.onChange(v => {
this.selectedTemplateId = v || null; this.selectedTemplateId = v || null;
this.schedulePreviewUpdate();
}); });
}); });
@ -369,7 +423,7 @@ export class ContextGeneratorModal extends Modal {
// === OUTPUT TARGETS === // === OUTPUT TARGETS ===
if (this.plugin.settings.targets.length > 0) { if (this.plugin.settings.targets.length > 0) {
configZone.createEl('h3', { text: 'Output Targets' }); this.targetsHeadingEl = configZone.createEl('h3', { text: 'Output Targets' });
const targetsContainer = configZone.createDiv({ cls: 'targets-checkboxes' }); const targetsContainer = configZone.createDiv({ cls: 'targets-checkboxes' });
targetsContainer.style.display = 'flex'; targetsContainer.style.display = 'flex';
@ -397,6 +451,7 @@ export class ContextGeneratorModal extends Modal {
} else { } else {
this.selectedTargetIds.delete(target.id); this.selectedTargetIds.delete(target.id);
} }
this.schedulePreviewUpdate();
}); });
const icon = row.createEl('span', { text: getTargetIcon(target.format) }); const icon = row.createEl('span', { text: getTargetIcon(target.format) });
@ -436,6 +491,42 @@ export class ContextGeneratorModal extends Modal {
// === TOKEN ESTIMATE === // === TOKEN ESTIMATE ===
this.renderTokenEstimate(configZone); this.renderTokenEstimate(configZone);
// =============================================
// PREVIEW PANE (full-width, between grid and footer)
// =============================================
const previewSection = contentEl.createDiv({
cls: `cc-section cc-preview-section${this.previewOpen ? '' : ' is-collapsed'}`,
});
const previewHeader = previewSection.createDiv({ cls: 'cc-section-header' });
const previewTitle = previewHeader.createEl('span', {
text: this.previewOpen ? 'Hide Preview' : 'Show Preview',
cls: 'cc-section-title',
});
previewHeader.createEl('span', { text: '\u203A', cls: 'cc-section-chevron' });
const previewBody = previewSection.createDiv({ cls: 'cc-section-content cc-preview-body' });
this.previewContent = previewBody.createEl('pre', { cls: 'cc-preview-code' });
this.previewLineInfo = previewBody.createDiv({ cls: 'cc-preview-line-info' });
previewHeader.addEventListener('click', async () => {
this.previewOpen = !this.previewOpen;
if (this.previewOpen) {
previewSection.removeClass('is-collapsed');
previewTitle.textContent = 'Hide Preview';
this.updatePreview();
} else {
previewSection.addClass('is-collapsed');
previewTitle.textContent = 'Show Preview';
}
this.plugin.settings.generatorPreviewOpen = this.previewOpen;
await this.plugin.saveSettings();
});
if (this.previewOpen) {
this.updatePreview();
}
// ============================================= // =============================================
// FOOTER (full-width, below both zones) // FOOTER (full-width, below both zones)
// ============================================= // =============================================
@ -507,6 +598,7 @@ export class ContextGeneratorModal extends Modal {
input.style.flex = '1'; input.style.flex = '1';
input.addEventListener('input', () => { input.addEventListener('input', () => {
folder.purpose = input.value; folder.purpose = input.value;
this.schedulePreviewUpdate();
}); });
} }
} }
@ -526,6 +618,7 @@ export class ContextGeneratorModal extends Modal {
nameInput.style.flex = '1'; nameInput.style.flex = '1';
nameInput.addEventListener('input', () => { nameInput.addEventListener('input', () => {
template.name = nameInput.value; template.name = nameInput.value;
this.schedulePreviewUpdate();
}); });
const folderInput = row.createEl('input', { type: 'text' }); const folderInput = row.createEl('input', { type: 'text' });
@ -534,6 +627,7 @@ export class ContextGeneratorModal extends Modal {
folderInput.style.flex = '1'; folderInput.style.flex = '1';
folderInput.addEventListener('input', () => { folderInput.addEventListener('input', () => {
template.folder = folderInput.value; template.folder = folderInput.value;
this.schedulePreviewUpdate();
}); });
const tagInput = row.createEl('input', { type: 'text' }); const tagInput = row.createEl('input', { type: 'text' });
@ -542,12 +636,14 @@ export class ContextGeneratorModal extends Modal {
tagInput.style.flex = '1'; tagInput.style.flex = '1';
tagInput.addEventListener('input', () => { tagInput.addEventListener('input', () => {
template.tag = tagInput.value; template.tag = tagInput.value;
this.schedulePreviewUpdate();
}); });
const removeBtn = row.createEl('button', { text: '✕' }); const removeBtn = row.createEl('button', { text: '✕' });
removeBtn.addEventListener('click', () => { removeBtn.addEventListener('click', () => {
this.config.templates.splice(index, 1); this.config.templates.splice(index, 1);
this.renderTemplates(container); this.renderTemplates(container);
this.schedulePreviewUpdate();
}); });
}); });
} }
@ -850,7 +946,125 @@ Here is the content with a link to ${link}.
`; `;
} }
private getPrimaryTarget(): OutputTarget | null {
const primaryId = this.plugin.settings.primaryTargetId;
const targets = this.plugin.settings.targets;
if (primaryId) {
return targets.find(t => t.id === primaryId && this.selectedTargetIds.has(t.id)) ?? null;
}
return targets.find(t => this.selectedTargetIds.has(t.id)) ?? null;
}
private static readonly FORMAT_LABELS: Record<OutputFormat, string> = {
markdown: 'Markdown',
xml: 'XML',
plain: 'Plain Text',
};
private updateStatusBar() {
if (!this.statusFileCount || !this.statusTokens || !this.statusTarget || !this.statusFormat) return;
// File count: VAULT.md + enabled optional files
const gf = this.config.generateFiles;
const fileCount = 1
+ (gf.conventions ? 1 : 0)
+ (gf.structure ? 1 : 0)
+ (gf.workflows ? 1 : 0)
+ (gf.templates ? 1 : 0)
+ (gf.examples ? 1 : 0);
this.statusFileCount.textContent = `${fileCount} file${fileCount !== 1 ? 's' : ''}`;
// Token estimate from generated output
const output = this.generatePreviewOutput();
const tokens = estimateTokens(output);
const primary = this.getPrimaryTarget();
const tokenText = `~${(tokens / 1000).toFixed(1)}k tokens`;
this.statusTokens.textContent = tokenText;
// Token warning color
if (primary && tokens > primary.maxTokens) {
this.statusTokens.addClass('is-warning');
} else {
this.statusTokens.removeClass('is-warning');
}
// Target name
this.statusTarget.textContent = primary
? `Target: ${primary.name}`
: 'Target: None';
// Format
if (primary) {
this.statusFormat.textContent = `Format: ${ContextGeneratorModal.FORMAT_LABELS[primary.format]}`;
} else {
this.statusFormat.textContent = 'Format: —';
}
}
private schedulePreviewUpdate() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
this.debounceTimer = null;
this.updatePreview();
this.updateStatusBar();
}, ContextGeneratorModal.DEBOUNCE_MS);
}
private updatePreview() {
if (!this.previewOpen || !this.previewContent || !this.previewLineInfo) return;
const fullOutput = this.generatePreviewOutput();
const allLines = fullOutput.split('\n');
const maxLines = ContextGeneratorModal.PREVIEW_LINES;
const truncated = allLines.length > maxLines;
const displayLines = truncated ? allLines.slice(0, maxLines) : allLines;
this.previewContent.textContent = displayLines.join('\n');
this.previewLineInfo.textContent = truncated
? `Showing ${maxLines} of ~${allLines.length} lines`
: `${allLines.length} lines`;
}
private generatePreviewOutput(): string {
const parts: string[] = [];
// VAULT.md is always included
parts.push(this.generateVaultMd());
if (this.config.generateFiles.conventions) {
parts.push(this.generateConventionsMd());
}
if (this.config.generateFiles.structure) {
parts.push(this.generateStructureMd());
}
if (this.config.generateFiles.workflows) {
parts.push(this.generateWorkflowsMd());
}
if (this.config.generateFiles.templates) {
parts.push(this.generateTemplatesMd());
}
if (this.config.generateFiles.examples) {
parts.push(this.generateExamplesMd());
}
return parts.join('\n\n---\n\n');
}
onClose() { onClose() {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
this.previewContent = null;
this.previewLineInfo = null;
this.statusFileCount = null;
this.statusTokens = null;
this.statusTarget = null;
this.statusFormat = null;
this.targetsHeadingEl = null;
const { contentEl } = this; const { contentEl } = this;
contentEl.empty(); contentEl.empty();
} }

View file

@ -25,6 +25,7 @@ export interface ClaudeContextSettings {
targetOutputFolder: string; targetOutputFolder: string;
lastSettingsTab: string; lastSettingsTab: string;
collapsedSections: string[]; collapsedSections: string[];
generatorPreviewOpen: boolean;
} }
export const DEFAULT_SETTINGS: ClaudeContextSettings = { export const DEFAULT_SETTINGS: ClaudeContextSettings = {
@ -44,6 +45,7 @@ export const DEFAULT_SETTINGS: ClaudeContextSettings = {
targetOutputFolder: '_claude/outputs', targetOutputFolder: '_claude/outputs',
lastSettingsTab: 'general', lastSettingsTab: 'general',
collapsedSections: [], collapsedSections: [],
generatorPreviewOpen: false,
}; };
export type SettingsTabId = 'general' | 'sources' | 'templates' | 'output' | 'history'; export type SettingsTabId = 'general' | 'sources' | 'templates' | 'output' | 'history';

View file

@ -88,7 +88,7 @@
.cc-gen-modal .modal-content { .cc-gen-modal .modal-content {
max-height: 80vh; max-height: 80vh;
overflow: hidden; overflow-y: auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
@ -131,6 +131,77 @@
margin-top: 8px; margin-top: 8px;
} }
/* Generator status bar */
.cc-gen-statusbar {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
margin-bottom: 12px;
background: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
border-radius: 4px;
font-size: 12px;
color: var(--text-muted);
}
.cc-statusbar-item {
white-space: nowrap;
}
.cc-statusbar-sep {
color: var(--text-faint);
user-select: none;
}
.cc-statusbar-clickable {
cursor: pointer;
border-radius: 3px;
padding: 1px 4px;
transition: background 0.15s ease, color 0.15s ease;
}
.cc-statusbar-clickable:hover {
background: var(--background-modifier-hover);
color: var(--text-normal);
}
.cc-statusbar-tokens.is-warning {
color: var(--text-warning, #e0a526);
font-weight: 600;
}
/* Generator preview pane */
.cc-preview-section {
margin-top: 12px;
}
.cc-preview-body {
border: 1px solid var(--background-modifier-border);
background: var(--background-secondary);
border-radius: 4px;
max-height: 300px;
overflow-y: auto;
padding: 10px;
}
.cc-preview-code {
font-family: var(--font-monospace);
font-size: 11px;
white-space: pre-wrap;
word-break: break-word;
background: transparent;
margin: 0;
}
.cc-preview-line-info {
font-size: 11px;
color: var(--text-muted);
font-style: italic;
text-align: right;
margin-top: 6px;
}
/* Stack on narrow viewports */ /* Stack on narrow viewports */
@media (max-width: 768px) { @media (max-width: 768px) {
.cc-gen-modal { .cc-gen-modal {