diff --git a/src/interview/parseInterviewConfig.ts b/src/interview/parseInterviewConfig.ts index d7585e7..fc01ed4 100644 --- a/src/interview/parseInterviewConfig.ts +++ b/src/interview/parseInterviewConfig.ts @@ -333,6 +333,17 @@ function parseStep(raw: Record): InterviewStep | null { step.prompt = raw.prompt.trim(); } + // Parse heading_level + if (raw.heading_level && typeof raw.heading_level === "object") { + const headingLevel = raw.heading_level as Record; + step.heading_level = { + enabled: headingLevel.enabled === true, + default: typeof headingLevel.default === "number" + ? Math.max(1, Math.min(6, headingLevel.default)) + : 2, + }; + } + // Parse output.template if (raw.output && typeof raw.output === "object") { const output = raw.output as Record; diff --git a/src/interview/renderer.ts b/src/interview/renderer.ts index e24a2f4..f7eddcc 100644 --- a/src/interview/renderer.ts +++ b/src/interview/renderer.ts @@ -93,17 +93,33 @@ function renderCaptureTextLine(step: CaptureTextLineStep, answers: RenderAnswers const text = String(value); + // Get heading level if configured + const headingLevelKey = `${step.key}_heading_level`; + const headingLevel = answers.collectedData.get(headingLevelKey); + let headingPrefix = ""; + if (step.heading_level?.enabled) { + if (typeof headingLevel === "number") { + const level = Math.max(1, Math.min(6, headingLevel)); + headingPrefix = "#".repeat(level) + " "; + } else if (step.heading_level.default) { + // Fallback to default if not set + const level = Math.max(1, Math.min(6, step.heading_level.default)); + headingPrefix = "#".repeat(level) + " "; + } + } + // Use template if provided if (step.output?.template) { return renderTemplate(step.output.template, { - text, + text: headingPrefix + text, field: step.key, - value: text, + value: headingPrefix + text, + heading_level: headingLevel ? String(headingLevel) : "", }); } - // Default: just the text - return text; + // Default: text with heading prefix if configured + return headingPrefix + text; } /** @@ -187,16 +203,32 @@ function renderLoopRecursive(step: LoopStep, answers: RenderAnswers, depth: numb const text = String(fieldValue); const captureStep = nestedStep as CaptureTextLineStep; + // Get heading level if configured (for nested loops, check in item data) + const headingLevelKey = `${nestedStep.key}_heading_level`; + const headingLevel = (item as Record)[headingLevelKey]; + let headingPrefix = ""; + if (captureStep.heading_level?.enabled) { + if (typeof headingLevel === "number") { + const level = Math.max(1, Math.min(6, headingLevel)); + headingPrefix = "#".repeat(level) + " "; + } else if (captureStep.heading_level.default) { + // Fallback to default if not set in item + const level = Math.max(1, Math.min(6, captureStep.heading_level.default)); + headingPrefix = "#".repeat(level) + " "; + } + } + // Use template if provided if (captureStep.output?.template) { itemParts.push(renderTemplate(captureStep.output.template, { - text, + text: headingPrefix + text, field: nestedStep.key, - value: text, + value: headingPrefix + text, + heading_level: headingLevel ? String(headingLevel) : (captureStep.heading_level?.default ? String(captureStep.heading_level.default) : ""), })); } else { - // Default: just the text - itemParts.push(text); + // Default: text with heading prefix if configured + itemParts.push(headingPrefix + text); } } } else if (nestedStep.type === "capture_frontmatter") { @@ -267,9 +299,15 @@ function renderLoopRecursive(step: LoopStep, answers: RenderAnswers, depth: numb * Render template string with token replacement. * Tokens: {text}, {field}, {value} */ -function renderTemplate(template: string, tokens: { text: string; field: string; value: string }): string { - return template +function renderTemplate(template: string, tokens: { text: string; field: string; value: string; heading_level?: string }): string { + let result = template .replace(/\{text\}/g, tokens.text) .replace(/\{field\}/g, tokens.field) .replace(/\{value\}/g, tokens.value); + + if (tokens.heading_level !== undefined) { + result = result.replace(/\{heading_level\}/g, tokens.heading_level); + } + + return result; } diff --git a/src/interview/types.ts b/src/interview/types.ts index bdd9173..6091842 100644 --- a/src/interview/types.ts +++ b/src/interview/types.ts @@ -80,8 +80,12 @@ export interface CaptureTextLineStep { label?: string; required?: boolean; prompt?: string; // Optional prompt text + heading_level?: { + enabled?: boolean; // Show heading level selector (default: false) + default?: number; // Default heading level 1-6 (default: 2) + }; output?: { - template?: string; // Template with tokens: {text}, {field}, {value} + template?: string; // Template with tokens: {text}, {field}, {value}, {heading_level} }; } diff --git a/src/ui/InterviewWizardModal.ts b/src/ui/InterviewWizardModal.ts index 5c1280f..dab5503 100644 --- a/src/ui/InterviewWizardModal.ts +++ b/src/ui/InterviewWizardModal.ts @@ -425,13 +425,70 @@ export class InterviewWizardModal extends Modal { }); } - // Input container + // Input container with heading level selector const inputContainer = fieldContainer.createEl("div", { cls: "mindnet-field__input", }); inputContainer.style.width = "100%"; + inputContainer.style.display = "flex"; + inputContainer.style.gap = "0.5em"; + inputContainer.style.alignItems = "center"; - const fieldSetting = new Setting(inputContainer); + // Heading level dropdown (if enabled) + let headingLevel: number | null = null; + const headingLevelKey = `${step.key}_heading_level`; + const storedHeadingLevel = this.state.collectedData.get(headingLevelKey); + if (storedHeadingLevel !== undefined && typeof storedHeadingLevel === "number") { + headingLevel = storedHeadingLevel; + } else if (step.heading_level?.enabled) { + headingLevel = step.heading_level.default || 2; + } + + if (step.heading_level?.enabled) { + const headingSelectorContainer = inputContainer.createEl("div"); + headingSelectorContainer.style.flexShrink = "0"; + + const headingLabel = headingSelectorContainer.createEl("label", { + text: "H", + attr: { for: `heading-level-${step.key}` }, + }); + headingLabel.style.marginRight = "0.25em"; + headingLabel.style.fontSize = "0.9em"; + headingLabel.style.color = "var(--text-muted)"; + + const headingSelect = headingSelectorContainer.createEl("select", { + attr: { id: `heading-level-${step.key}` }, + }); + headingSelect.style.padding = "0.25em 0.5em"; + headingSelect.style.border = "1px solid var(--background-modifier-border)"; + headingSelect.style.borderRadius = "4px"; + headingSelect.style.background = "var(--background-primary)"; + headingSelect.style.fontSize = "0.9em"; + headingSelect.style.minWidth = "3em"; + + // Add options H1-H6 + for (let level = 1; level <= 6; level++) { + const option = headingSelect.createEl("option", { + text: `H${level}`, + attr: { value: String(level) }, + }); + if (headingLevel === level) { + option.selected = true; + } + } + + headingSelect.onchange = () => { + const selectedLevel = parseInt(headingSelect.value, 10); + this.state.collectedData.set(headingLevelKey, selectedLevel); + }; + } + + // Text input (takes remaining space) + const textInputContainer = inputContainer.createEl("div"); + textInputContainer.style.flex = "1"; + textInputContainer.style.minWidth = "0"; + + const fieldSetting = new Setting(textInputContainer); fieldSetting.settingEl.style.width = "100%"; fieldSetting.controlEl.style.width = "100%"; @@ -1146,13 +1203,82 @@ export class InterviewWizardModal extends Modal { }); } - // Input container + // Input container with heading level selector const inputContainer = fieldContainer.createEl("div", { cls: "mindnet-field__input", }); inputContainer.style.width = "100%"; + inputContainer.style.display = "flex"; + inputContainer.style.gap = "0.5em"; + inputContainer.style.alignItems = "center"; - const fieldSetting = new Setting(inputContainer); + // Heading level dropdown (if enabled) + const headingLevelDraftKey = `${nestedStep.key}_heading_level`; + // Get loopState from the loopKey to access draft + const currentLoopState = this.state.loopRuntimeStates.get(loopKey); + let headingLevelDraftValue: number | undefined = undefined; + if (currentLoopState) { + headingLevelDraftValue = currentLoopState.draft[headingLevelDraftKey] as number | undefined; + // If not in draft and we're editing, try to get from the item being edited + if (headingLevelDraftValue === undefined && currentLoopState.editIndex !== null) { + const itemBeingEdited = currentLoopState.items[currentLoopState.editIndex]; + if (itemBeingEdited && typeof itemBeingEdited === "object") { + headingLevelDraftValue = (itemBeingEdited as Record)[headingLevelDraftKey] as number | undefined; + } + } + } + let headingLevel: number | null = null; + if (headingLevelDraftValue !== undefined && typeof headingLevelDraftValue === "number") { + headingLevel = headingLevelDraftValue; + } else if (nestedStep.heading_level?.enabled) { + headingLevel = nestedStep.heading_level.default || 2; + } + + if (nestedStep.heading_level?.enabled) { + const headingSelectorContainer = inputContainer.createEl("div"); + headingSelectorContainer.style.flexShrink = "0"; + + const headingLabel = headingSelectorContainer.createEl("label", { + text: "H", + attr: { for: `heading-level-${loopKey}-${nestedStep.key}` }, + }); + headingLabel.style.marginRight = "0.25em"; + headingLabel.style.fontSize = "0.9em"; + headingLabel.style.color = "var(--text-muted)"; + + const headingSelect = headingSelectorContainer.createEl("select", { + attr: { id: `heading-level-${loopKey}-${nestedStep.key}` }, + }); + headingSelect.style.padding = "0.25em 0.5em"; + headingSelect.style.border = "1px solid var(--background-modifier-border)"; + headingSelect.style.borderRadius = "4px"; + headingSelect.style.background = "var(--background-primary)"; + headingSelect.style.fontSize = "0.9em"; + headingSelect.style.minWidth = "3em"; + + // Add options H1-H6 + for (let level = 1; level <= 6; level++) { + const option = headingSelect.createEl("option", { + text: `H${level}`, + attr: { value: String(level) }, + }); + if (headingLevel === level) { + option.selected = true; + } + } + + headingSelect.onchange = () => { + const selectedLevel = parseInt(headingSelect.value, 10); + onFieldChange(headingLevelDraftKey, selectedLevel); + }; + } + + // Text input (takes remaining space) + const textInputContainer = inputContainer.createEl("div"); + textInputContainer.style.flex = "1"; + textInputContainer.style.minWidth = "0"; + + const fieldSetting = new Setting(textInputContainer); fieldSetting.settingEl.style.width = "100%"; fieldSetting.controlEl.style.width = "100%";