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.
This commit is contained in:
parent
bab84549e2
commit
d7aa9bd964
|
|
@ -49,6 +49,7 @@ export interface CaptureFrontmatterStep {
|
|||
label?: string;
|
||||
field: string;
|
||||
required?: boolean;
|
||||
prompt?: string; // Optional prompt text
|
||||
}
|
||||
|
||||
export interface CaptureTextStep {
|
||||
|
|
|
|||
|
|
@ -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,13 +430,45 @@ 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) => {
|
||||
// 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);
|
||||
|
|
@ -324,6 +499,8 @@ export class InterviewWizardModal extends Modal {
|
|||
this.state.patches.push(patch);
|
||||
}
|
||||
});
|
||||
text.inputEl.style.width = "100%";
|
||||
text.inputEl.style.boxSizing = "border-box";
|
||||
text.inputEl.focus();
|
||||
});
|
||||
}
|
||||
|
|
@ -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,19 +633,61 @@ 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) => {
|
||||
// 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);
|
||||
|
||||
|
|
@ -439,12 +695,16 @@ export class InterviewWizardModal extends Modal {
|
|||
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,26 +764,72 @@ 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", {
|
||||
// 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) => {
|
||||
// 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",
|
||||
cls: "interview-note",
|
||||
|
|
@ -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
317
src/ui/markdownToolbar.ts
Normal 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;
|
||||
}
|
||||
226
styles.css
226
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user