Implement Interview Wizard Modal enhancements with new CSS styles and markdown support
Some checks are pending
Node.js build / build (20.x) (push) Waiting to run
Node.js build / build (22.x) (push) Waiting to run

- 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.
This commit is contained in:
Lars 2026-01-16 14:11:04 +01:00
parent bab84549e2
commit d7aa9bd964
4 changed files with 940 additions and 85 deletions

View File

@ -49,6 +49,7 @@ export interface CaptureFrontmatterStep {
label?: string;
field: string;
required?: boolean;
prompt?: string; // Optional prompt text
}
export interface CaptureTextStep {

View File

@ -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<string, string> = new Map();
// Store preview mode state per step key
private previewMode: Map<string, boolean> = 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,
@ -108,6 +120,9 @@ export class InterviewWizardModal extends Modal {
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,19 +257,73 @@ 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);
@ -256,10 +338,71 @@ export class InterviewWizardModal extends Modal {
// 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<void> {
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",
});
// Show prompt if available
if (nestedStep.prompt) {
containerEl.createEl("p", {
text: nestedStep.prompt,
cls: "interview-prompt",
// Label
if (nestedStep.label) {
const labelEl = fieldContainer.createEl("div", {
cls: "mindnet-field__label",
text: nestedStep.label,
});
}
new Setting(containerEl).addTextArea((text) => {
// Description/Prompt
if (nestedStep.prompt) {
const descEl = fieldContainer.createEl("div", {
cls: "mindnet-field__desc",
text: nestedStep.prompt,
});
}
// 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)

317
src/ui/markdownToolbar.ts Normal file
View File

@ -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;
}

View File

@ -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;
}