5 tasks covering undo/redo, drag & drop, live preview, and styles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
739 lines
18 KiB
Markdown
739 lines
18 KiB
Markdown
# Phase 2 — Builder Upgrade Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** Transform the FormBuilderModal with drag & drop, live preview, and undo/redo.
|
|
|
|
**Architecture:** All three features modify `form-builder.ts`. Undo/redo is implemented first (provides infrastructure), then drag & drop (uses pushSnapshot), then layout restructure for preview. Each task builds on the previous.
|
|
|
|
**Tech Stack:** TypeScript, Obsidian API, HTML5 Drag & Drop, esbuild
|
|
|
|
---
|
|
|
|
### Task 1: Add undo/redo infrastructure
|
|
|
|
**Files:**
|
|
- Modify: `src/ui/form-builder.ts`
|
|
|
|
**Step 1: Add instance variables and imports**
|
|
|
|
At the top of `FormBuilderModal` class, after the existing `private onSave` line, add:
|
|
|
|
```typescript
|
|
private history: FormDefinition[] = [];
|
|
private historyIndex = 0;
|
|
```
|
|
|
|
**Step 2: Initialize history in constructor**
|
|
|
|
In the constructor, after `this.onSave = onSave;`, add:
|
|
|
|
```typescript
|
|
this.history = [structuredClone(this.draft)];
|
|
this.historyIndex = 0;
|
|
```
|
|
|
|
**Step 3: Add pushSnapshot, undo, redo methods**
|
|
|
|
Add these three methods after the `onClose()` method, before `render()`:
|
|
|
|
```typescript
|
|
private pushSnapshot(): void {
|
|
// Truncate any forward history
|
|
this.history = this.history.slice(0, this.historyIndex + 1);
|
|
this.history.push(structuredClone(this.draft));
|
|
// Cap at 30 entries
|
|
if (this.history.length > 30) {
|
|
this.history.shift();
|
|
} else {
|
|
this.historyIndex++;
|
|
}
|
|
}
|
|
|
|
private undo(): void {
|
|
if (this.historyIndex <= 0) return;
|
|
this.historyIndex--;
|
|
this.draft = structuredClone(this.history[this.historyIndex]);
|
|
this.render();
|
|
}
|
|
|
|
private redo(): void {
|
|
if (this.historyIndex >= this.history.length - 1) return;
|
|
this.historyIndex++;
|
|
this.draft = structuredClone(this.history[this.historyIndex]);
|
|
this.render();
|
|
}
|
|
```
|
|
|
|
**Step 4: Add keyboard listener in render()**
|
|
|
|
In the `render()` method, right after `contentEl.addClass('ff-builder-modal');`, add:
|
|
|
|
```typescript
|
|
// Undo/Redo keyboard shortcuts
|
|
contentEl.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
if (e.key === 'z' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
|
e.preventDefault();
|
|
this.undo();
|
|
} else if (e.key === 'z' && (e.ctrlKey || e.metaKey) && e.shiftKey) {
|
|
e.preventDefault();
|
|
this.redo();
|
|
}
|
|
});
|
|
```
|
|
|
|
**Step 5: Add undo/redo buttons in footer**
|
|
|
|
In the `render()` method, replace the entire footer section (from `const footer = ...` through the save button click handler) with:
|
|
|
|
```typescript
|
|
// --- Footer ---
|
|
const footer = contentEl.createDiv({ cls: 'ff-builder-footer' });
|
|
|
|
// Left side: undo/redo
|
|
const undoRedoEl = footer.createDiv({ cls: 'ff-builder-undo-redo' });
|
|
|
|
const undoBtn = undoRedoEl.createEl('button', {
|
|
cls: 'ff-builder-action-btn',
|
|
text: '\u21A9',
|
|
});
|
|
undoBtn.setAttribute('aria-label', 'Undo (Ctrl+Z)');
|
|
if (this.historyIndex <= 0) undoBtn.setAttribute('disabled', '');
|
|
undoBtn.addEventListener('click', () => this.undo());
|
|
|
|
const redoBtn = undoRedoEl.createEl('button', {
|
|
cls: 'ff-builder-action-btn',
|
|
text: '\u21AA',
|
|
});
|
|
redoBtn.setAttribute('aria-label', 'Redo (Ctrl+Shift+Z)');
|
|
if (this.historyIndex >= this.history.length - 1) redoBtn.setAttribute('disabled', '');
|
|
redoBtn.addEventListener('click', () => this.redo());
|
|
|
|
// Right side: cancel/save
|
|
const footerRight = footer.createDiv({ cls: 'ff-builder-footer-right' });
|
|
|
|
const cancelBtn = footerRight.createEl('button', { text: 'Cancel' });
|
|
cancelBtn.addEventListener('click', () => {
|
|
this.close();
|
|
});
|
|
|
|
const saveBtn = footerRight.createEl('button', {
|
|
text: 'Save',
|
|
cls: 'mod-cta',
|
|
});
|
|
saveBtn.addEventListener('click', () => {
|
|
if (!this.draft.name.trim()) {
|
|
new Notice('Form name cannot be empty.');
|
|
return;
|
|
}
|
|
this.onSave(this.draft);
|
|
this.close();
|
|
});
|
|
```
|
|
|
|
**Step 6: Add pushSnapshot to all mutation points**
|
|
|
|
In the `render()` method:
|
|
|
|
1. Before the mode dropdown onChange mutation, add `this.pushSnapshot();` before `this.draft.mode = v as ...`:
|
|
```typescript
|
|
(v) => {
|
|
this.pushSnapshot();
|
|
this.draft.mode = v as 'create' | 'update';
|
|
this.render();
|
|
},
|
|
```
|
|
|
|
2. Before the add-field mutation, add `this.pushSnapshot();` before `this.draft.fields.push(newField)`:
|
|
```typescript
|
|
addBtn.addEventListener('click', () => {
|
|
const newField: FormField = {
|
|
id: `field_${Date.now()}`,
|
|
label: 'New Field',
|
|
type: 'text',
|
|
required: false,
|
|
};
|
|
this.pushSnapshot();
|
|
this.draft.fields.push(newField);
|
|
this.render();
|
|
});
|
|
```
|
|
|
|
In the `renderFields()` method:
|
|
|
|
3. Before move-up swap: add `this.pushSnapshot();`
|
|
```typescript
|
|
upBtn.addEventListener('click', () => {
|
|
this.pushSnapshot();
|
|
[this.draft.fields[i - 1], this.draft.fields[i]] = [
|
|
this.draft.fields[i],
|
|
this.draft.fields[i - 1],
|
|
];
|
|
this.render();
|
|
});
|
|
```
|
|
|
|
4. Before move-down swap: add `this.pushSnapshot();`
|
|
```typescript
|
|
downBtn.addEventListener('click', () => {
|
|
this.pushSnapshot();
|
|
[this.draft.fields[i], this.draft.fields[i + 1]] = [
|
|
this.draft.fields[i + 1],
|
|
this.draft.fields[i],
|
|
];
|
|
this.render();
|
|
});
|
|
```
|
|
|
|
5. Before delete splice: add `this.pushSnapshot();`
|
|
```typescript
|
|
deleteBtn.addEventListener('click', () => {
|
|
this.pushSnapshot();
|
|
this.draft.fields.splice(i, 1);
|
|
this.render();
|
|
});
|
|
```
|
|
|
|
6. Before type change: add `this.pushSnapshot();`
|
|
```typescript
|
|
(v) => {
|
|
this.pushSnapshot();
|
|
field.type = v as FieldType;
|
|
this.render();
|
|
},
|
|
```
|
|
|
|
**Step 7: Add pushSnapshot to setting helpers**
|
|
|
|
Modify each helper to call `this.pushSnapshot()` before invoking the callback:
|
|
|
|
In `addTextSetting`:
|
|
```typescript
|
|
input.addEventListener('change', () => {
|
|
this.pushSnapshot();
|
|
onChange(input.value);
|
|
});
|
|
```
|
|
|
|
In `addTextareaSetting`:
|
|
```typescript
|
|
textarea.addEventListener('change', () => {
|
|
this.pushSnapshot();
|
|
onChange(textarea.value);
|
|
});
|
|
```
|
|
|
|
In `addDropdownSetting`:
|
|
```typescript
|
|
select.addEventListener('change', () => {
|
|
this.pushSnapshot();
|
|
onChange(select.value);
|
|
});
|
|
```
|
|
|
|
In `addToggleSetting`:
|
|
```typescript
|
|
toggle.addEventListener('click', () => {
|
|
this.pushSnapshot();
|
|
const enabled = !toggle.hasClass('is-enabled');
|
|
toggle.toggleClass('is-enabled', enabled);
|
|
onChange(enabled);
|
|
});
|
|
```
|
|
|
|
**Step 8: Build to verify**
|
|
|
|
Run: `node esbuild.config.mjs`
|
|
Expected: No errors
|
|
|
|
**Step 9: Commit**
|
|
|
|
```bash
|
|
git add src/ui/form-builder.ts
|
|
git commit -m "feat: add snapshot-based undo/redo to form builder"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 2: Add drag & drop field reordering
|
|
|
|
**Files:**
|
|
- Modify: `src/ui/form-builder.ts` (renderFields method)
|
|
|
|
**Step 1: Add instance variable**
|
|
|
|
After the `historyIndex` declaration, add:
|
|
|
|
```typescript
|
|
private dragSourceIndex: number | null = null;
|
|
```
|
|
|
|
**Step 2: Add drag handle and events to renderFields()**
|
|
|
|
In the `renderFields()` method, make these changes to each field element:
|
|
|
|
After `const fieldEl = container.createDiv({ cls: 'ff-builder-field' });`, add:
|
|
```typescript
|
|
fieldEl.setAttribute('draggable', 'true');
|
|
fieldEl.dataset.index = String(i);
|
|
```
|
|
|
|
In the header section, add a drag handle BEFORE the field number span:
|
|
```typescript
|
|
headerEl.createSpan({
|
|
cls: 'ff-drag-handle',
|
|
text: '\u2807',
|
|
});
|
|
```
|
|
|
|
After the delete button (after the entire actions section), add drag event handlers on `fieldEl`:
|
|
|
|
```typescript
|
|
// --- Drag & Drop ---
|
|
fieldEl.addEventListener('dragstart', (e: DragEvent) => {
|
|
this.dragSourceIndex = i;
|
|
fieldEl.addClass('ff-dragging');
|
|
if (e.dataTransfer) {
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
}
|
|
});
|
|
|
|
fieldEl.addEventListener('dragend', () => {
|
|
this.dragSourceIndex = null;
|
|
fieldEl.removeClass('ff-dragging');
|
|
});
|
|
|
|
fieldEl.addEventListener('dragover', (e: DragEvent) => {
|
|
e.preventDefault();
|
|
if (this.dragSourceIndex === null || this.dragSourceIndex === i) return;
|
|
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
|
|
|
|
const rect = fieldEl.getBoundingClientRect();
|
|
const midY = rect.top + rect.height / 2;
|
|
fieldEl.removeClass('ff-drop-above', 'ff-drop-below');
|
|
if (e.clientY < midY) {
|
|
fieldEl.addClass('ff-drop-above');
|
|
} else {
|
|
fieldEl.addClass('ff-drop-below');
|
|
}
|
|
});
|
|
|
|
fieldEl.addEventListener('dragleave', () => {
|
|
fieldEl.removeClass('ff-drop-above', 'ff-drop-below');
|
|
});
|
|
|
|
fieldEl.addEventListener('drop', (e: DragEvent) => {
|
|
e.preventDefault();
|
|
fieldEl.removeClass('ff-drop-above', 'ff-drop-below');
|
|
if (this.dragSourceIndex === null || this.dragSourceIndex === i) return;
|
|
|
|
const rect = fieldEl.getBoundingClientRect();
|
|
const midY = rect.top + rect.height / 2;
|
|
const insertBefore = e.clientY < midY;
|
|
|
|
this.pushSnapshot();
|
|
const [moved] = this.draft.fields.splice(this.dragSourceIndex, 1);
|
|
let targetIndex = insertBefore ? i : i + 1;
|
|
if (this.dragSourceIndex < i) targetIndex--;
|
|
this.draft.fields.splice(targetIndex, 0, moved);
|
|
this.dragSourceIndex = null;
|
|
this.render();
|
|
});
|
|
```
|
|
|
|
**Step 3: Build to verify**
|
|
|
|
Run: `node esbuild.config.mjs`
|
|
Expected: No errors
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add src/ui/form-builder.ts
|
|
git commit -m "feat: add drag & drop field reordering to form builder"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 3: Add side-by-side live preview
|
|
|
|
**Files:**
|
|
- Modify: `src/ui/form-builder.ts`
|
|
|
|
**Step 1: Add import for renderField**
|
|
|
|
At the top of the file, change the imports to include renderField:
|
|
|
|
```typescript
|
|
import { App, Modal, Notice } from 'obsidian';
|
|
import { FormDefinition, FormField, FieldType } from '../types';
|
|
import { renderField } from './field-renderers';
|
|
```
|
|
|
|
**Step 2: Restructure render() to use grid layout**
|
|
|
|
In the `render()` method, replace the content between the keyboard listener and the footer with a grid layout. The full `render()` method should become:
|
|
|
|
```typescript
|
|
private render(): void {
|
|
const { contentEl } = this;
|
|
contentEl.empty();
|
|
contentEl.addClass('ff-builder-modal');
|
|
|
|
// Undo/Redo keyboard shortcuts
|
|
contentEl.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
if (e.key === 'z' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
|
|
e.preventDefault();
|
|
this.undo();
|
|
} else if (e.key === 'z' && (e.ctrlKey || e.metaKey) && e.shiftKey) {
|
|
e.preventDefault();
|
|
this.redo();
|
|
}
|
|
});
|
|
|
|
// --- Two-column layout ---
|
|
const layout = contentEl.createDiv({ cls: 'ff-builder-layout' });
|
|
|
|
// === LEFT: Editor ===
|
|
const editorEl = layout.createDiv({ cls: 'ff-builder-editor' });
|
|
|
|
editorEl.createEl('h2', { text: 'Form Settings' });
|
|
|
|
const generalEl = editorEl.createDiv({ cls: 'ff-builder-general' });
|
|
|
|
// Name
|
|
this.addTextSetting(generalEl, 'Form Name', this.draft.name, (v) => {
|
|
this.draft.name = v;
|
|
});
|
|
|
|
// Mode
|
|
this.addDropdownSetting(
|
|
generalEl,
|
|
'Mode',
|
|
['create', 'update'],
|
|
this.draft.mode,
|
|
(v) => {
|
|
this.pushSnapshot();
|
|
this.draft.mode = v as 'create' | 'update';
|
|
this.render();
|
|
},
|
|
);
|
|
|
|
// Mode-specific settings
|
|
if (this.draft.mode === 'create') {
|
|
this.addTextSetting(
|
|
generalEl,
|
|
'Target Folder',
|
|
this.draft.targetFolder ?? '/',
|
|
(v) => {
|
|
this.draft.targetFolder = v;
|
|
},
|
|
);
|
|
this.addTextSetting(
|
|
generalEl,
|
|
'File Name Template',
|
|
this.draft.fileNameTemplate ?? '{{date}}-{{title}}',
|
|
(v) => {
|
|
this.draft.fileNameTemplate = v;
|
|
},
|
|
);
|
|
this.addTextareaSetting(
|
|
generalEl,
|
|
'Body Template',
|
|
this.draft.bodyTemplate ?? '',
|
|
(v) => {
|
|
this.draft.bodyTemplate = v;
|
|
},
|
|
);
|
|
} else {
|
|
this.addDropdownSetting(
|
|
generalEl,
|
|
'Target File',
|
|
['active', 'prompt'],
|
|
this.draft.targetFile ?? 'active',
|
|
(v) => {
|
|
this.draft.targetFile = v as 'active' | 'prompt';
|
|
},
|
|
);
|
|
}
|
|
|
|
// Fields section
|
|
editorEl.createEl('h3', { text: 'Fields' });
|
|
|
|
const fieldsContainer = editorEl.createDiv({ cls: 'ff-builder-fields' });
|
|
this.renderFields(fieldsContainer);
|
|
|
|
// Add field button
|
|
const addBtn = editorEl.createEl('button', {
|
|
cls: 'ff-builder-add-btn',
|
|
text: '+ Add Field',
|
|
});
|
|
addBtn.addEventListener('click', () => {
|
|
const newField: FormField = {
|
|
id: `field_${Date.now()}`,
|
|
label: 'New Field',
|
|
type: 'text',
|
|
required: false,
|
|
};
|
|
this.pushSnapshot();
|
|
this.draft.fields.push(newField);
|
|
this.render();
|
|
});
|
|
|
|
// === RIGHT: Preview ===
|
|
const previewEl = layout.createDiv({ cls: 'ff-builder-preview' });
|
|
previewEl.createEl('h2', { text: 'Preview' });
|
|
|
|
const previewContent = previewEl.createDiv({ cls: 'ff-builder-preview-content' });
|
|
|
|
if (this.draft.fields.length === 0) {
|
|
previewContent.createDiv({
|
|
cls: 'ff-builder-preview-empty',
|
|
text: 'Add fields to see a preview.',
|
|
});
|
|
} else {
|
|
// Form title
|
|
previewContent.createEl('h3', {
|
|
text: this.draft.name || 'Untitled Form',
|
|
cls: 'ff-form-title',
|
|
});
|
|
|
|
// Render each field
|
|
const previewFields = previewContent.createDiv({ cls: 'ff-fields' });
|
|
for (const field of this.draft.fields) {
|
|
renderField(this.app, previewFields, field, field.defaultValue);
|
|
}
|
|
|
|
// Disabled submit button
|
|
const previewFooter = previewContent.createDiv({ cls: 'ff-form-footer' });
|
|
const previewSubmit = previewFooter.createEl('button', {
|
|
text: this.draft.mode === 'create' ? 'Create Note' : 'Update Frontmatter',
|
|
cls: 'mod-cta ff-submit-btn',
|
|
});
|
|
previewSubmit.setAttribute('disabled', '');
|
|
}
|
|
|
|
// --- Footer (full width, below grid) ---
|
|
const footer = contentEl.createDiv({ cls: 'ff-builder-footer' });
|
|
|
|
// Left side: undo/redo
|
|
const undoRedoEl = footer.createDiv({ cls: 'ff-builder-undo-redo' });
|
|
|
|
const undoBtn = undoRedoEl.createEl('button', {
|
|
cls: 'ff-builder-action-btn',
|
|
text: '\u21A9',
|
|
});
|
|
undoBtn.setAttribute('aria-label', 'Undo (Ctrl+Z)');
|
|
if (this.historyIndex <= 0) undoBtn.setAttribute('disabled', '');
|
|
undoBtn.addEventListener('click', () => this.undo());
|
|
|
|
const redoBtn = undoRedoEl.createEl('button', {
|
|
cls: 'ff-builder-action-btn',
|
|
text: '\u21AA',
|
|
});
|
|
redoBtn.setAttribute('aria-label', 'Redo (Ctrl+Shift+Z)');
|
|
if (this.historyIndex >= this.history.length - 1) redoBtn.setAttribute('disabled', '');
|
|
redoBtn.addEventListener('click', () => this.redo());
|
|
|
|
// Right side: cancel/save
|
|
const footerRight = footer.createDiv({ cls: 'ff-builder-footer-right' });
|
|
|
|
const cancelBtn = footerRight.createEl('button', { text: 'Cancel' });
|
|
cancelBtn.addEventListener('click', () => {
|
|
this.close();
|
|
});
|
|
|
|
const saveBtn = footerRight.createEl('button', {
|
|
text: 'Save',
|
|
cls: 'mod-cta',
|
|
});
|
|
saveBtn.addEventListener('click', () => {
|
|
if (!this.draft.name.trim()) {
|
|
new Notice('Form name cannot be empty.');
|
|
return;
|
|
}
|
|
this.onSave(this.draft);
|
|
this.close();
|
|
});
|
|
}
|
|
```
|
|
|
|
Note: This replaces the ENTIRE render() method. The renderFields() method and setting helpers stay unchanged from Task 2.
|
|
|
|
**Step 3: Build to verify**
|
|
|
|
Run: `node esbuild.config.mjs`
|
|
Expected: No errors
|
|
|
|
**Step 4: Commit**
|
|
|
|
```bash
|
|
git add src/ui/form-builder.ts
|
|
git commit -m "feat: add side-by-side live preview to form builder"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 4: Add Phase 2 styles
|
|
|
|
**Files:**
|
|
- Modify: `styles.css`
|
|
|
|
**Step 1: Add drag & drop styles**
|
|
|
|
Add after the existing `.ff-builder-field-body` rule (after `padding: 10px;`):
|
|
|
|
```css
|
|
/* --- Drag & Drop --- */
|
|
|
|
.ff-drag-handle {
|
|
cursor: grab;
|
|
color: var(--text-faint);
|
|
font-size: 1rem;
|
|
line-height: 1;
|
|
user-select: none;
|
|
transition: color 0.15s ease;
|
|
padding: 0 2px;
|
|
}
|
|
|
|
.ff-drag-handle:hover {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
.ff-builder-field.ff-dragging {
|
|
opacity: 0.4;
|
|
}
|
|
|
|
.ff-builder-field.ff-drop-above {
|
|
border-top: 2px solid var(--interactive-accent);
|
|
}
|
|
|
|
.ff-builder-field.ff-drop-below {
|
|
border-bottom: 2px solid var(--interactive-accent);
|
|
}
|
|
```
|
|
|
|
**Step 2: Update builder modal width and add layout styles**
|
|
|
|
Replace the existing `.ff-builder-modal` rule:
|
|
|
|
```css
|
|
.ff-builder-modal {
|
|
max-width: 1100px;
|
|
margin: 0 auto;
|
|
}
|
|
```
|
|
|
|
Add after `.ff-builder-modal h3`:
|
|
|
|
```css
|
|
/* --- Two-column layout --- */
|
|
|
|
.ff-builder-layout {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 24px;
|
|
min-height: 300px;
|
|
}
|
|
|
|
.ff-builder-editor {
|
|
min-width: 0;
|
|
}
|
|
```
|
|
|
|
**Step 3: Add preview panel styles**
|
|
|
|
Add after the `.ff-builder-editor` rule:
|
|
|
|
```css
|
|
/* --- Preview panel --- */
|
|
|
|
.ff-builder-preview {
|
|
border-left: 1px solid var(--background-modifier-border);
|
|
padding-left: 24px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.ff-builder-preview h2 {
|
|
position: sticky;
|
|
top: 0;
|
|
background: var(--background-primary);
|
|
padding-bottom: 8px;
|
|
z-index: 1;
|
|
}
|
|
|
|
.ff-builder-preview-content {
|
|
max-height: 55vh;
|
|
overflow-y: auto;
|
|
padding-right: 4px;
|
|
}
|
|
|
|
.ff-builder-preview-empty {
|
|
padding: 32px 16px;
|
|
text-align: center;
|
|
font-size: 0.85rem;
|
|
color: var(--text-muted);
|
|
}
|
|
```
|
|
|
|
**Step 4: Add undo/redo footer styles**
|
|
|
|
Replace the existing `.ff-builder-footer` rule:
|
|
|
|
```css
|
|
.ff-builder-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-top: 16px;
|
|
padding-top: 12px;
|
|
border-top: 1px solid var(--background-modifier-border);
|
|
}
|
|
|
|
.ff-builder-undo-redo {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
|
|
.ff-builder-footer-right {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.ff-builder-action-btn:disabled {
|
|
opacity: 0.3;
|
|
cursor: not-allowed;
|
|
}
|
|
```
|
|
|
|
**Step 5: Build to verify**
|
|
|
|
Run: `node esbuild.config.mjs`
|
|
Expected: No errors
|
|
|
|
**Step 6: Commit**
|
|
|
|
```bash
|
|
git add styles.css
|
|
git commit -m "feat: add styles for drag & drop, preview panel, and undo/redo"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 5: Final build and verification
|
|
|
|
**Step 1: Clean build**
|
|
|
|
Run: `node esbuild.config.mjs`
|
|
Expected: Build succeeds, no errors
|
|
|
|
**Step 2: Verify all files are tracked**
|
|
|
|
Run: `git status`
|
|
Expected: Clean working tree
|
|
|
|
**Step 3: Verify commit history**
|
|
|
|
Run: `git log --oneline -6`
|
|
Expected: 4 new commits on top of Phase 1 history
|