From d7aa9bd96484d093da985661379eb55b2c2bb71f Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 16 Jan 2026 14:11:04 +0100 Subject: [PATCH] Implement Interview Wizard Modal enhancements with new CSS styles and markdown support - Added comprehensive CSS styles for the Interview Wizard Modal, improving layout and responsiveness. - Introduced markdown editing capabilities with a toolbar for text areas, allowing for rich text input. - Enhanced step rendering logic to support optional prompt text for each step. - Updated the modal structure to include sticky navigation and improved content organization. - Refactored existing rendering methods to utilize new CSS classes for better styling consistency. --- src/interview/types.ts | 1 + src/ui/InterviewWizardModal.ts | 481 +++++++++++++++++++++++++++------ src/ui/markdownToolbar.ts | 317 ++++++++++++++++++++++ styles.css | 226 ++++++++++++++++ 4 files changed, 940 insertions(+), 85 deletions(-) create mode 100644 src/ui/markdownToolbar.ts diff --git a/src/interview/types.ts b/src/interview/types.ts index 127c99c..044b735 100644 --- a/src/interview/types.ts +++ b/src/interview/types.ts @@ -49,6 +49,7 @@ export interface CaptureFrontmatterStep { label?: string; field: string; required?: boolean; + prompt?: string; // Optional prompt text } export interface CaptureTextStep { diff --git a/src/ui/InterviewWizardModal.ts b/src/ui/InterviewWizardModal.ts index e10c45a..ce8a05c 100644 --- a/src/ui/InterviewWizardModal.ts +++ b/src/ui/InterviewWizardModal.ts @@ -1,5 +1,7 @@ import { App, + Component, + MarkdownRenderer, Modal, Notice, Setting, @@ -20,6 +22,11 @@ import { flattenSteps, } from "../interview/wizardState"; import { extractFrontmatterId } from "../parser/parseFrontmatter"; +import { + createMarkdownToolbar, + applyMarkdownWrap, + applyLinePrefix, +} from "./markdownToolbar"; export interface WizardResult { applied: boolean; @@ -35,6 +42,8 @@ export class InterviewWizardModal extends Modal { profileKey: string; // Store current input values to save on navigation private currentInputValues: Map = new Map(); + // Store preview mode state per step key + private previewMode: Map = new Map(); constructor( app: App, @@ -94,6 +103,9 @@ export class InterviewWizardModal extends Modal { onOpen(): void { const fileName = this.file.basename || this.file.name.replace(/\.md$/, ""); + // Add CSS class for styling + this.modalEl.addClass("mindnet-wizard-modal"); + console.log("=== WIZARD START ===", { profileKey: this.profileKey, file: this.file.path, @@ -107,7 +119,10 @@ export class InterviewWizardModal extends Modal { renderStep(): void { const { contentEl } = this; contentEl.empty(); - + + // Apply flex layout structure + contentEl.addClass("modal-content"); + const step = getCurrentStep(this.state); console.log("Render step", { @@ -141,11 +156,16 @@ export class InterviewWizardModal extends Modal { return; } + // Create body container for scrollable content + const bodyEl = contentEl.createEl("div", { + cls: "modal-content-body", + }); + // Check if ID exists const hasId = this.checkIdExists(); if (!hasId) { - const warningEl = contentEl.createEl("div", { + const warningEl = bodyEl.createEl("div", { cls: "interview-warning", }); warningEl.createEl("p", { @@ -158,30 +178,38 @@ export class InterviewWizardModal extends Modal { }); } + // Create step content container + const stepContentEl = bodyEl.createEl("div", { + cls: "step-content", + }); + // Render step based on type switch (step.type) { case "instruction": - this.renderInstructionStep(step, contentEl); + this.renderInstructionStep(step, stepContentEl); break; case "capture_text": - this.renderCaptureTextStep(step, contentEl); + this.renderCaptureTextStep(step, stepContentEl); break; case "capture_frontmatter": - this.renderCaptureFrontmatterStep(step, contentEl); + this.renderCaptureFrontmatterStep(step, stepContentEl); break; case "loop": - this.renderLoopStep(step, contentEl); + this.renderLoopStep(step, stepContentEl); break; case "llm_dialog": - this.renderLLMDialogStep(step, contentEl); + this.renderLLMDialogStep(step, stepContentEl); break; case "review": - this.renderReviewStep(step, contentEl); + this.renderReviewStep(step, stepContentEl); break; } - // Navigation buttons - this.renderNavigation(contentEl); + // Navigation buttons in sticky footer + const footerEl = contentEl.createEl("div", { + cls: "modal-content-footer", + }); + this.renderNavigation(footerEl); } checkIdExists(): boolean { @@ -229,37 +257,152 @@ export class InterviewWizardModal extends Modal { const existingValue = (this.state.collectedData.get(step.key) as string) || ""; + const isPreviewMode = this.previewMode.get(step.key) || false; console.log("Render capture_text step", { stepKey: step.key, stepLabel: step.label, existingValue: existingValue, valueLength: existingValue.length, + isPreviewMode: isPreviewMode, }); - containerEl.createEl("h2", { - text: step.label || "Enter Text", + // Field container with vertical layout + const fieldContainer = containerEl.createEl("div", { + cls: "mindnet-field", }); - new Setting(containerEl).addTextArea((text) => { + // Label + if (step.label) { + const labelEl = fieldContainer.createEl("div", { + cls: "mindnet-field__label", + text: step.label, + }); + } + + // Description/Prompt + if (step.prompt) { + const descEl = fieldContainer.createEl("div", { + cls: "mindnet-field__desc", + text: step.prompt, + }); + } + + // Container for editor/preview + const editorContainer = fieldContainer.createEl("div", { + cls: "markdown-editor-container", + }); + editorContainer.style.width = "100%"; + editorContainer.style.position = "relative"; + + // Preview container (hidden by default) + const previewContainer = editorContainer.createEl("div", { + cls: "markdown-preview-container", + }); + previewContainer.style.display = isPreviewMode ? "block" : "none"; + previewContainer.style.width = "100%"; + previewContainer.style.minHeight = "240px"; + previewContainer.style.padding = "1em"; + previewContainer.style.border = "1px solid var(--background-modifier-border)"; + previewContainer.style.borderRadius = "4px"; + previewContainer.style.background = "var(--background-primary)"; + previewContainer.style.overflowY = "auto"; + + // Editor container + const textEditorContainer = editorContainer.createEl("div", { + cls: "markdown-editor-wrapper", + }); + textEditorContainer.style.display = isPreviewMode ? "none" : "block"; + textEditorContainer.style.width = "100%"; + + // Create textarea first + const textSetting = new Setting(textEditorContainer); + textSetting.settingEl.style.width = "100%"; + textSetting.controlEl.style.width = "100%"; + + let textareaRef: HTMLTextAreaElement | null = null; + + textSetting.addTextArea((text) => { + textareaRef = text.inputEl; text.setValue(existingValue); // Store initial value this.currentInputValues.set(step.key, existingValue); - + text.onChange((value) => { console.log("Text field changed", { stepKey: step.key, valueLength: value.length, valuePreview: value.substring(0, 50) + (value.length > 50 ? "..." : ""), }); - + // Update stored value this.currentInputValues.set(step.key, value); this.state.collectedData.set(step.key, value); + + // Update preview if in preview mode + if (isPreviewMode) { + this.updatePreview(previewContainer, value); + } }); text.inputEl.rows = 10; + text.inputEl.style.width = "100%"; + text.inputEl.style.minHeight = "240px"; + text.inputEl.style.boxSizing = "border-box"; text.inputEl.focus(); }); + + // Create toolbar after textarea is created + // Use setTimeout to ensure textarea is in DOM + setTimeout(() => { + const textarea = textEditorContainer.querySelector("textarea"); + if (textarea) { + const toolbar = createMarkdownToolbar( + textarea, + () => { + const newPreviewMode = !this.previewMode.get(step.key); + this.previewMode.set(step.key, newPreviewMode); + this.renderStep(); + } + ); + textEditorContainer.insertBefore(toolbar, textEditorContainer.firstChild); + } + }, 10); + + // Render preview if in preview mode + if (isPreviewMode && existingValue) { + this.updatePreview(previewContainer, existingValue); + } + } + + /** + * Update preview container with rendered markdown. + */ + private async updatePreview( + container: HTMLElement, + markdown: string + ): Promise { + container.empty(); + if (!markdown.trim()) { + container.createEl("p", { + text: "(empty)", + cls: "text-muted", + }); + return; + } + + // Use Obsidian's MarkdownRenderer + // Create a component for the renderer + const component = new Component(); + // Register it with the modal (Modal extends Component) + (this as any).addChild(component); + + await MarkdownRenderer.render( + this.app, + markdown, + container, + this.file.path, + component + ); } renderCaptureFrontmatterStep( @@ -287,45 +430,79 @@ export class InterviewWizardModal extends Modal { defaultValue: defaultValue, }); - containerEl.createEl("h2", { - text: step.label || `Enter ${step.field}`, + // Field container with vertical layout + const fieldContainer = containerEl.createEl("div", { + cls: "mindnet-field", }); - new Setting(containerEl) - .setName(step.field) - .addText((text) => { - text.setValue(defaultValue); - // Store initial value - this.currentInputValues.set(step.key, defaultValue); - - text.onChange((value) => { - console.log("Frontmatter field changed", { - stepKey: step.key, - field: step.field, - value: value, - }); - - // Update stored value - this.currentInputValues.set(step.key, value); - - this.state.collectedData.set(step.key, value); - // Update or add patch - const existingPatchIndex = this.state.patches.findIndex( - p => p.type === "frontmatter" && p.field === step.field - ); - const patch = { - type: "frontmatter" as const, - field: step.field!, - value: value, - }; - if (existingPatchIndex >= 0) { - this.state.patches[existingPatchIndex] = patch; - } else { - this.state.patches.push(patch); - } - }); - text.inputEl.focus(); + // Label + const labelText = step.label || step.field; + if (labelText) { + const labelEl = fieldContainer.createEl("div", { + cls: "mindnet-field__label", + text: labelText, }); + } + + // Description/Prompt + if (step.prompt) { + const descEl = fieldContainer.createEl("div", { + cls: "mindnet-field__desc", + text: step.prompt, + }); + } + + // Input container + const inputContainer = fieldContainer.createEl("div", { + cls: "mindnet-field__input", + }); + inputContainer.style.width = "100%"; + + const fieldSetting = new Setting(inputContainer); + fieldSetting.settingEl.style.width = "100%"; + fieldSetting.controlEl.style.width = "100%"; + + // Hide the default label from Setting component + const settingNameEl2 = fieldSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null; + if (settingNameEl2) { + settingNameEl2.style.display = "none"; + } + + fieldSetting.addText((text) => { + text.setValue(defaultValue); + // Store initial value + this.currentInputValues.set(step.key, defaultValue); + + text.onChange((value) => { + console.log("Frontmatter field changed", { + stepKey: step.key, + field: step.field, + value: value, + }); + + // Update stored value + this.currentInputValues.set(step.key, value); + + this.state.collectedData.set(step.key, value); + // Update or add patch + const existingPatchIndex = this.state.patches.findIndex( + p => p.type === "frontmatter" && p.field === step.field + ); + const patch = { + type: "frontmatter" as const, + field: step.field!, + value: value, + }; + if (existingPatchIndex >= 0) { + this.state.patches[existingPatchIndex] = patch; + } else { + this.state.patches.push(patch); + } + }); + text.inputEl.style.width = "100%"; + text.inputEl.style.boxSizing = "border-box"; + text.inputEl.focus(); + }); } renderLoopStep(step: InterviewStep, containerEl: HTMLElement): void { @@ -350,6 +527,7 @@ export class InterviewWizardModal extends Modal { // Show existing items if (items.length > 0) { const itemsList = containerEl.createEl("div", { cls: "loop-items-list" }); + itemsList.style.width = "100%"; itemsList.createEl("h3", { text: `Gesammelte Items (${items.length}):` }); const ul = itemsList.createEl("ul"); for (let i = 0; i < items.length; i++) { @@ -383,7 +561,12 @@ export class InterviewWizardModal extends Modal { // Render nested steps for current item if (step.items.length > 0) { - containerEl.createEl("h3", { + const itemFormContainer = containerEl.createEl("div", { + cls: "loop-item-form", + }); + itemFormContainer.style.width = "100%"; + + itemFormContainer.createEl("h3", { text: items.length === 0 ? "First Item" : `Item ${items.length + 1}`, }); @@ -397,19 +580,50 @@ export class InterviewWizardModal extends Modal { const existingValue = (itemData.get(nestedStep.key) as string) || ""; const inputKey = `${itemDataKey}_${nestedStep.key}`; - containerEl.createEl("h4", { - text: nestedStep.label || nestedStep.key, + // Field container with vertical layout + const fieldContainer = itemFormContainer.createEl("div", { + cls: "mindnet-field", }); + + // Label + if (nestedStep.label) { + const labelEl = fieldContainer.createEl("div", { + cls: "mindnet-field__label", + text: nestedStep.label, + }); + } - // Show prompt if available + // Description/Prompt if (nestedStep.prompt) { - containerEl.createEl("p", { + const descEl = fieldContainer.createEl("div", { + cls: "mindnet-field__desc", text: nestedStep.prompt, - cls: "interview-prompt", }); } - new Setting(containerEl).addTextArea((text) => { + // Editor container + const editorContainer = fieldContainer.createEl("div", { + cls: "markdown-editor-container", + }); + editorContainer.style.width = "100%"; + editorContainer.style.position = "relative"; + + const textEditorContainer = editorContainer.createEl("div", { + cls: "markdown-editor-wrapper", + }); + textEditorContainer.style.width = "100%"; + + const textSetting = new Setting(textEditorContainer); + textSetting.settingEl.style.width = "100%"; + textSetting.controlEl.style.width = "100%"; + + // Hide the default label from Setting component + const settingNameEl = textSetting.settingEl.querySelector(".setting-item-name") as HTMLElement; + if (settingNameEl) { + settingNameEl.style.display = "none"; + } + + textSetting.addTextArea((text) => { text.setValue(existingValue); this.currentInputValues.set(inputKey, existingValue); @@ -419,32 +633,78 @@ export class InterviewWizardModal extends Modal { }); text.inputEl.rows = 10; text.inputEl.style.width = "100%"; - text.inputEl.style.minHeight = "150px"; + text.inputEl.style.minHeight = "200px"; + text.inputEl.style.boxSizing = "border-box"; }); + + // Add toolbar for loop item textarea + setTimeout(() => { + const textarea = textEditorContainer.querySelector("textarea"); + if (textarea) { + const itemToolbar = createMarkdownToolbar(textarea); + textEditorContainer.insertBefore(itemToolbar, textEditorContainer.firstChild); + } + }, 10); } else if (nestedStep.type === "capture_frontmatter") { const existingValue = (itemData.get(nestedStep.key) as string) || ""; const inputKey = `${itemDataKey}_${nestedStep.key}`; - containerEl.createEl("h4", { - text: nestedStep.label || nestedStep.field || nestedStep.key, + // Field container with vertical layout + const fieldContainer = itemFormContainer.createEl("div", { + cls: "mindnet-field", }); - new Setting(containerEl) - .setName(nestedStep.field || nestedStep.key) - .addText((text) => { - text.setValue(existingValue); - this.currentInputValues.set(inputKey, existingValue); - - text.onChange((value) => { - this.currentInputValues.set(inputKey, value); - itemData.set(nestedStep.key, value); - }); + // Label + const labelText = nestedStep.label || nestedStep.field || nestedStep.key; + if (labelText) { + const labelEl = fieldContainer.createEl("div", { + cls: "mindnet-field__label", + text: labelText, }); + } + + // Description/Prompt + if (nestedStep.prompt) { + const descEl = fieldContainer.createEl("div", { + cls: "mindnet-field__desc", + text: nestedStep.prompt, + }); + } + + // Input container + const inputContainer = fieldContainer.createEl("div", { + cls: "mindnet-field__input", + }); + inputContainer.style.width = "100%"; + + const fieldSetting = new Setting(inputContainer); + fieldSetting.settingEl.style.width = "100%"; + fieldSetting.controlEl.style.width = "100%"; + + // Hide the default label from Setting component + const settingNameEl = fieldSetting.settingEl.querySelector(".setting-item-name") as HTMLElement | null; + if (settingNameEl) { + settingNameEl.style.display = "none"; + } + + fieldSetting.addText((text) => { + text.setValue(existingValue); + this.currentInputValues.set(inputKey, existingValue); + + text.onChange((value) => { + this.currentInputValues.set(inputKey, value); + itemData.set(nestedStep.key, value); + }); + text.inputEl.style.width = "100%"; + text.inputEl.style.boxSizing = "border-box"; + }); } } // Add item button - new Setting(containerEl).addButton((button) => { + const addButtonSetting = new Setting(itemFormContainer); + addButtonSetting.settingEl.style.width = "100%"; + addButtonSetting.addButton((button) => { button .setButtonText("Add Item") .setCta() @@ -493,7 +753,7 @@ export class InterviewWizardModal extends Modal { // Show hint if no items yet if (items.length === 0) { - containerEl.createEl("p", { + itemFormContainer.createEl("p", { text: "⚠️ Please add at least one item before continuing", cls: "interview-warning", }); @@ -504,25 +764,71 @@ export class InterviewWizardModal extends Modal { renderLLMDialogStep(step: InterviewStep, containerEl: HTMLElement): void { if (step.type !== "llm_dialog") return; - containerEl.createEl("h2", { - text: step.label || "LLM Dialog", + // Field container with vertical layout + const fieldContainer = containerEl.createEl("div", { + cls: "mindnet-field", }); - containerEl.createEl("p", { - text: `Prompt: ${step.prompt}`, - }); + // Label + if (step.label) { + const labelEl = fieldContainer.createEl("div", { + cls: "mindnet-field__label", + text: step.label, + }); + } + + // Description/Prompt + if (step.prompt) { + const descEl = fieldContainer.createEl("div", { + cls: "mindnet-field__desc", + text: `Prompt: ${step.prompt}`, + }); + } const existingValue = (this.state.collectedData.get(step.key) as string) || ""; - new Setting(containerEl) - .setName("Response") - .addTextArea((text) => { - text.setValue(existingValue).onChange((value) => { - this.state.collectedData.set(step.key, value); - }); - text.inputEl.rows = 10; + // Editor container for LLM response + const llmEditorContainer = fieldContainer.createEl("div", { + cls: "markdown-editor-wrapper", + }); + llmEditorContainer.style.width = "100%"; + + const llmSetting = new Setting(llmEditorContainer); + llmSetting.settingEl.style.width = "100%"; + llmSetting.controlEl.style.width = "100%"; + + // Hide the default label from Setting component + const settingNameEl = llmSetting.settingEl.querySelector(".setting-item-name") as HTMLElement; + if (settingNameEl) { + settingNameEl.style.display = "none"; + } + + let llmTextareaRef: HTMLTextAreaElement | null = null; + + llmSetting.addTextArea((text) => { + llmTextareaRef = text.inputEl; + text.setValue(existingValue); + this.currentInputValues.set(step.key, existingValue); + + text.onChange((value) => { + this.currentInputValues.set(step.key, value); + this.state.collectedData.set(step.key, value); }); + text.inputEl.rows = 10; + text.inputEl.style.width = "100%"; + text.inputEl.style.minHeight = "240px"; + text.inputEl.style.boxSizing = "border-box"; + }); + + // Add toolbar for LLM response + setTimeout(() => { + const textarea = llmEditorContainer.querySelector("textarea"); + if (textarea) { + const llmToolbar = createMarkdownToolbar(textarea); + llmEditorContainer.insertBefore(llmToolbar, llmEditorContainer.firstChild); + } + }, 10); containerEl.createEl("p", { text: "Note: LLM dialog requires manual input in this version", @@ -564,9 +870,14 @@ export class InterviewWizardModal extends Modal { } renderNavigation(containerEl: HTMLElement): void { + // Navigation buttons in a flex row const navContainer = containerEl.createEl("div", { cls: "interview-navigation", }); + navContainer.style.display = "flex"; + navContainer.style.gap = "0.5em"; + navContainer.style.justifyContent = "flex-end"; + navContainer.style.flexWrap = "wrap"; // Back button new Setting(navContainer) diff --git a/src/ui/markdownToolbar.ts b/src/ui/markdownToolbar.ts new file mode 100644 index 0000000..92dd62e --- /dev/null +++ b/src/ui/markdownToolbar.ts @@ -0,0 +1,317 @@ +/** + * Markdown toolbar helpers for text editors. + */ + +/** + * Apply markdown formatting to selected text in textarea. + * If no selection, inserts before/after at cursor position. + */ +export function applyMarkdownWrap( + textarea: HTMLTextAreaElement, + before: string, + after: string = "" +): void { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const text = textarea.value; + const selectedText = text.substring(start, end); + + let newText: string; + let newCursorPos: number; + + if (selectedText) { + // Wrap selected text + newText = text.substring(0, start) + before + selectedText + after + text.substring(end); + newCursorPos = start + before.length + selectedText.length + after.length; + } else { + // Insert at cursor position + newText = text.substring(0, start) + before + after + text.substring(end); + newCursorPos = start + before.length; + } + + textarea.value = newText; + textarea.setSelectionRange(newCursorPos, newCursorPos); + textarea.focus(); + + // Trigger change event + textarea.dispatchEvent(new Event("input", { bubbles: true })); +} + +/** + * Apply prefix to each selected line (for lists). + */ +export function applyLinePrefix( + textarea: HTMLTextAreaElement, + prefix: string +): void { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const text = textarea.value; + + // Find line boundaries + const beforeSelection = text.substring(0, start); + const selection = text.substring(start, end); + const afterSelection = text.substring(end); + + const lineStart = beforeSelection.lastIndexOf("\n") + 1; + const lineEnd = afterSelection.indexOf("\n"); + const fullLineEnd = end + (lineEnd >= 0 ? lineEnd : afterSelection.length); + + // Get all lines in selection + const lines = selection.split("\n"); + const firstLineStart = beforeSelection.lastIndexOf("\n") + 1; + const firstLinePrefix = text.substring(firstLineStart, start); + + // Apply prefix to each line + const prefixedLines = lines.map((line) => { + if (line.trim()) { + return prefix + line; + } + return line; + }); + + const newSelection = prefixedLines.join("\n"); + const newText = + text.substring(0, lineStart) + newSelection + text.substring(fullLineEnd); + + textarea.value = newText; + + // Restore selection + const newStart = lineStart; + const newEnd = lineStart + newSelection.length; + textarea.setSelectionRange(newStart, newEnd); + textarea.focus(); + + // Trigger change event + textarea.dispatchEvent(new Event("input", { bubbles: true })); +} + +/** + * Remove prefix from each selected line (for lists). + */ +export function removeLinePrefix( + textarea: HTMLTextAreaElement, + prefix: string +): void { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const text = textarea.value; + + // Find line boundaries + const beforeSelection = text.substring(0, start); + const selection = text.substring(start, end); + const afterSelection = text.substring(end); + + const lineStart = beforeSelection.lastIndexOf("\n") + 1; + const lineEnd = afterSelection.indexOf("\n"); + const fullLineEnd = end + (lineEnd >= 0 ? lineEnd : afterSelection.length); + + // Get all lines in selection + const lines = selection.split("\n"); + + // Remove prefix from each line + const unprefixedLines = lines.map((line) => { + if (line.startsWith(prefix)) { + return line.substring(prefix.length); + } + return line; + }); + + const newSelection = unprefixedLines.join("\n"); + const newText = + text.substring(0, lineStart) + newSelection + text.substring(fullLineEnd); + + textarea.value = newText; + + // Restore selection + const newStart = lineStart; + const newEnd = lineStart + newSelection.length; + textarea.setSelectionRange(newStart, newEnd); + textarea.focus(); + + // Trigger change event + textarea.dispatchEvent(new Event("input", { bubbles: true })); +} + +/** + * Create markdown toolbar with buttons. + */ +export function createMarkdownToolbar( + textarea: HTMLTextAreaElement, + onTogglePreview?: () => void +): HTMLElement { + const toolbar = document.createElement("div"); + toolbar.className = "markdown-toolbar"; + toolbar.style.display = "flex"; + toolbar.style.gap = "0.25em"; + toolbar.style.padding = "0.5em"; + toolbar.style.borderBottom = "1px solid var(--background-modifier-border)"; + toolbar.style.flexWrap = "wrap"; + toolbar.style.alignItems = "center"; + + // Bold + const boldBtn = createToolbarButton("B", "Bold (Ctrl+B)", () => { + applyMarkdownWrap(textarea, "**", "**"); + }); + toolbar.appendChild(boldBtn); + + // Italic + const italicBtn = createToolbarButton("I", "Italic (Ctrl+I)", () => { + applyMarkdownWrap(textarea, "*", "*"); + }); + toolbar.appendChild(italicBtn); + + // H2 + const h2Btn = createToolbarButton("H2", "Heading 2", () => { + applyLinePrefix(textarea, "## "); + }); + toolbar.appendChild(h2Btn); + + // H3 + const h3Btn = createToolbarButton("H3", "Heading 3", () => { + applyLinePrefix(textarea, "### "); + }); + toolbar.appendChild(h3Btn); + + // Bullet List + const bulletBtn = createToolbarButton("•", "Bullet List", () => { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const text = textarea.value; + const selectedText = text.substring(start, end); + + // Check if lines already have bullet prefix + const lines = selectedText.split("\n"); + const hasBullets = lines.some((line) => line.trim().startsWith("- ")); + + if (hasBullets) { + removeLinePrefix(textarea, "- "); + } else { + applyLinePrefix(textarea, "- "); + } + }); + toolbar.appendChild(bulletBtn); + + // Numbered List + const numberedBtn = createToolbarButton("1.", "Numbered List", () => { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const text = textarea.value; + const selectedText = text.substring(start, end); + + // Check if lines already have numbered prefix + const lines = selectedText.split("\n"); + const hasNumbers = lines.some((line) => /^\d+\.\s/.test(line.trim())); + + if (hasNumbers) { + // Remove numbered prefix (simple version - removes "1. " pattern) + const unprefixedLines = lines.map((line) => { + const match = line.match(/^(\d+\.\s)(.*)$/); + return match ? match[2] : line; + }); + const newSelection = unprefixedLines.join("\n"); + textarea.value = + text.substring(0, start) + newSelection + text.substring(end); + textarea.setSelectionRange(start, start + newSelection.length); + textarea.focus(); + textarea.dispatchEvent(new Event("input", { bubbles: true })); + } else { + // Add numbered prefix + let counter = 1; + const numberedLines = lines.map((line) => { + if (line.trim()) { + return `${counter++}. ${line}`; + } + return line; + }); + const newSelection = numberedLines.join("\n"); + textarea.value = + text.substring(0, start) + newSelection + text.substring(end); + textarea.setSelectionRange(start, start + newSelection.length); + textarea.focus(); + textarea.dispatchEvent(new Event("input", { bubbles: true })); + } + }); + toolbar.appendChild(numberedBtn); + + // Code + const codeBtn = createToolbarButton("", "Code (Ctrl+`)", () => { + applyMarkdownWrap(textarea, "`", "`"); + }); + toolbar.appendChild(codeBtn); + + // Link + const linkBtn = createToolbarButton("🔗", "Link", () => { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const text = textarea.value; + const selectedText = text.substring(start, end); + + if (selectedText) { + // Wrap selected text as link + applyMarkdownWrap(textarea, "[", "](url)"); + // Select "url" part + setTimeout(() => { + const newPos = textarea.selectionStart - 5; // "url)".length + textarea.setSelectionRange(newPos, newPos + 3); + }, 0); + } else { + // Insert link template + applyMarkdownWrap(textarea, "[text](url)", ""); + // Select "text" part + setTimeout(() => { + const newPos = textarea.selectionStart - 9; // "](url)".length + textarea.setSelectionRange(newPos, newPos + 4); + }, 0); + } + }); + toolbar.appendChild(linkBtn); + + // Preview toggle + if (onTogglePreview) { + const previewBtn = createToolbarButton("👁️", "Toggle Preview", () => { + onTogglePreview(); + }); + previewBtn.style.marginLeft = "auto"; + toolbar.appendChild(previewBtn); + } + + return toolbar; +} + +/** + * Create a toolbar button. + */ +function createToolbarButton( + label: string, + title: string, + onClick: () => void +): HTMLElement { + const button = document.createElement("button"); + button.textContent = label; + button.title = title; + button.className = "markdown-toolbar-button"; + button.style.padding = "0.25em 0.5em"; + button.style.border = "1px solid var(--background-modifier-border)"; + button.style.borderRadius = "4px"; + button.style.background = "var(--background-primary)"; + button.style.cursor = "pointer"; + button.style.fontSize = "0.9em"; + button.style.minWidth = "2em"; + + button.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + onClick(); + }); + + button.addEventListener("mouseenter", () => { + button.style.background = "var(--interactive-hover)"; + }); + + button.addEventListener("mouseleave", () => { + button.style.background = "var(--background-primary)"; + }); + + return button; +} diff --git a/styles.css b/styles.css index 369d727..0c378bd 100644 --- a/styles.css +++ b/styles.css @@ -61,3 +61,229 @@ If your plugin does not need CSS, delete this file. min-height: 150px; resize: vertical; } + +/* Interview Wizard Modal Layout */ +.mindnet-wizard-modal.modal { + width: clamp(720px, 88vw, 1100px) !important; + height: clamp(640px, 86vh, 920px) !important; + max-width: 90vw !important; + max-height: 90vh !important; +} + +.mindnet-wizard-modal .modal-container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} + +.mindnet-wizard-modal .modal-content { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow: hidden; +} + +.mindnet-wizard-modal .modal-content-header { + flex-shrink: 0; + padding: 1em; + border-bottom: 1px solid var(--background-modifier-border); +} + +.mindnet-wizard-modal .modal-content-body { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 1.5em; + min-height: 0; /* Important for flex scrolling */ +} + +.mindnet-wizard-modal .modal-content-footer { + flex-shrink: 0; + padding: 1em; + border-top: 1px solid var(--background-modifier-border); + background-color: var(--background-primary); + position: sticky; + bottom: 0; + z-index: 10; +} + +/* Full-width inputs in wizard */ +.mindnet-wizard-modal .setting-item { + width: 100%; +} + +.mindnet-wizard-modal .setting-item-control { + width: 100%; +} + +.mindnet-wizard-modal .setting-item-control textarea { + width: 100% !important; + min-height: 240px; + flex-grow: 1; + resize: vertical; + box-sizing: border-box; +} + +.mindnet-wizard-modal .setting-item-control input[type="text"] { + width: 100% !important; + box-sizing: border-box; +} + +/* Single column layout for step content */ +.mindnet-wizard-modal .step-content { + display: flex; + flex-direction: column; + width: 100%; + gap: 1em; +} + +.mindnet-wizard-modal .step-content .setting-item { + width: 100%; + margin: 0.5em 0; +} + +/* Navigation buttons layout */ +.mindnet-wizard-modal .interview-navigation { + display: flex; + gap: 0.5em; + justify-content: flex-end; + flex-wrap: wrap; +} + +.mindnet-wizard-modal .interview-navigation .setting-item { + margin: 0; + flex: 0 0 auto; +} + +/* Markdown toolbar styles */ +.markdown-toolbar { + display: flex; + gap: 0.25em; + padding: 0.5em; + border-bottom: 1px solid var(--background-modifier-border); + flex-wrap: wrap; + align-items: center; + background-color: var(--background-secondary); + border-radius: 4px 4px 0 0; +} + +.markdown-toolbar-button { + padding: 0.25em 0.5em; + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + background: var(--background-primary); + cursor: pointer; + font-size: 0.9em; + min-width: 2em; + transition: background-color 0.1s; +} + +.markdown-toolbar-button:hover { + background: var(--interactive-hover); +} + +.markdown-toolbar-button:active { + background: var(--interactive-active); +} + +/* Markdown editor container */ +.markdown-editor-container { + width: 100%; + position: relative; +} + +.markdown-editor-wrapper { + width: 100%; + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + overflow: hidden; +} + +.markdown-preview-container { + width: 100%; + min-height: 240px; + padding: 1em; + border: 1px solid var(--background-modifier-border); + border-radius: 4px; + background: var(--background-primary); + overflow-y: auto; +} + +.markdown-preview-container .markdown-preview-view { + width: 100%; +} + +/* Ensure textarea in editor wrapper takes full width */ +.markdown-editor-wrapper .setting-item { + width: 100%; + margin: 0; + border: none; +} + +.markdown-editor-wrapper .setting-item-control { + width: 100%; +} + +.markdown-editor-wrapper textarea { + border: none !important; + border-radius: 0 !important; + padding: 1em !important; +} + +/* Field container with vertical layout */ +.mindnet-field { + display: flex; + flex-direction: column; + gap: 8px; + margin: 14px 0 18px; + width: 100%; +} + +.mindnet-field__label { + font-weight: 600; + font-size: 16px; + line-height: 1.2; + display: block; + width: 100%; +} + +.mindnet-field__desc { + font-size: 13px; + opacity: 0.85; + line-height: 1.35; + white-space: normal; + overflow: visible; + text-overflow: clip; + display: block; + width: 100%; + word-wrap: break-word; + overflow-wrap: break-word; +} + +.mindnet-field__input { + width: 100%; + display: block; +} + +.mindnet-field textarea, +.mindnet-field input[type="text"] { + width: 100% !important; + box-sizing: border-box; +} + +/* Ensure no truncation in field descriptions */ +.mindnet-field__desc, +.mindnet-field__label { + white-space: normal !important; + overflow: visible !important; + text-overflow: clip !important; +} + +/* Remove any truncation from Setting component labels when inside mindnet-field */ +.mindnet-field .setting-item-name { + white-space: normal !important; + overflow: visible !important; + text-overflow: clip !important; +}