# Phase 1 — Quick Wins Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Add three new field types (slider, color, time), keyboard navigation, and form import/export to the Formfire Obsidian plugin. **Architecture:** Extends the existing RenderedField pattern for new field types, adds a keydown event delegate on the form modal, and introduces a new `form-io.ts` utility module for JSON serialization. All changes integrate with existing patterns — no new abstractions needed. **Tech Stack:** TypeScript, Obsidian API, esbuild --- ### Task 1: Extend the type system **Files:** - Modify: `src/types.ts:5-15` (FieldType union) - Modify: `src/types.ts:17-28` (FormField interface) **Step 1: Add new field types to FieldType union** In `src/types.ts`, replace the FieldType union: ```typescript export type FieldType = | 'text' | 'textarea' | 'number' | 'toggle' | 'date' | 'dropdown' | 'tags' | 'note-link' | 'folder-picker' | 'rating' | 'slider' | 'color' | 'time'; ``` **Step 2: Add slider properties to FormField** In `src/types.ts`, add to the FormField interface after the `folder` property: ```typescript /** For slider: minimum value. */ min?: number; /** For slider: maximum value. */ max?: number; /** For slider: step increment. */ step?: number; ``` **Step 3: Build to verify** Run: `node esbuild.config.mjs` Expected: No errors (new types are additive) **Step 4: Commit** ```bash git add src/types.ts git commit -m "feat: add slider, color, time to FieldType and slider props to FormField" ``` --- ### Task 2: Add slider renderer **Files:** - Modify: `src/ui/field-renderers.ts` **Step 1: Add renderSlider function** Add before the `renderNoteLink` function (around line 265): ```typescript function renderSlider( controlEl: HTMLDivElement, field: FormField, initialValue?: FieldValue, ): { getValue: () => FieldValue; setValue: (v: FieldValue) => void } { const min = field.min ?? 0; const max = field.max ?? 100; const step = field.step ?? 1; const startVal = initialValue !== undefined ? Number(initialValue) : min; const wrapper = controlEl.createDiv({ cls: 'ff-slider-wrapper' }); const input = wrapper.createEl('input', { cls: 'ff-slider', type: 'range', }); input.min = String(min); input.max = String(max); input.step = String(step); input.value = String(startVal); const valueLabel = wrapper.createSpan({ cls: 'ff-slider-value', text: String(startVal), }); input.addEventListener('input', () => { valueLabel.textContent = input.value; }); return { getValue: () => Number(input.value), setValue: (v) => { input.value = String(v); valueLabel.textContent = String(v); }, }; } ``` **Step 2: Add case to renderField switch** In the `renderField` function's switch statement, add before the `default` case: ```typescript case 'slider': accessor = renderSlider(controlEl, field, initialValue); break; ``` **Step 3: Build to verify** Run: `node esbuild.config.mjs` Expected: No errors **Step 4: Commit** ```bash git add src/ui/field-renderers.ts git commit -m "feat: add slider field renderer with live value display" ``` --- ### Task 3: Add color renderer **Files:** - Modify: `src/ui/field-renderers.ts` **Step 1: Add renderColor function** Add after the `renderSlider` function: ```typescript function renderColor( controlEl: HTMLDivElement, _field: FormField, initialValue?: FieldValue, ): { getValue: () => FieldValue; setValue: (v: FieldValue) => void } { const startVal = initialValue !== undefined ? String(initialValue) : '#000000'; const wrapper = controlEl.createDiv({ cls: 'ff-color-wrapper' }); const input = wrapper.createEl('input', { cls: 'ff-color-input', type: 'color', }); input.value = startVal; const hexLabel = wrapper.createSpan({ cls: 'ff-color-hex', text: startVal, }); input.addEventListener('input', () => { hexLabel.textContent = input.value; }); return { getValue: () => input.value, setValue: (v) => { input.value = String(v); hexLabel.textContent = String(v); }, }; } ``` **Step 2: Add case to renderField switch** ```typescript case 'color': accessor = renderColor(controlEl, field, initialValue); break; ``` **Step 3: Build to verify** Run: `node esbuild.config.mjs` Expected: No errors **Step 4: Commit** ```bash git add src/ui/field-renderers.ts git commit -m "feat: add color picker field renderer with hex display" ``` --- ### Task 4: Add time renderer **Files:** - Modify: `src/ui/field-renderers.ts` **Step 1: Add renderTime function** Add after the `renderColor` function: ```typescript function renderTime( controlEl: HTMLDivElement, field: FormField, initialValue?: FieldValue, ): { getValue: () => FieldValue; setValue: (v: FieldValue) => void } { const input = controlEl.createEl('input', { cls: 'ff-input', type: 'time', placeholder: field.placeholder ?? '', }); if (initialValue !== undefined) { input.value = String(initialValue); } return { getValue: () => input.value, setValue: (v) => { input.value = String(v); }, }; } ``` **Step 2: Add case to renderField switch** ```typescript case 'time': accessor = renderTime(controlEl, field, initialValue); break; ``` **Step 3: Build to verify** Run: `node esbuild.config.mjs` Expected: No errors **Step 4: Commit** ```bash git add src/ui/field-renderers.ts git commit -m "feat: add time picker field renderer" ``` --- ### Task 5: Add slider validation **Files:** - Modify: `src/utils/validators.ts:32-44` **Step 1: Add slider range validation** In `validateForm`, add after the rating validation block (after line 43): ```typescript if (field.type === 'slider') { const n = Number(value); const min = field.min ?? 0; const max = field.max ?? 100; if (isNaN(n) || n < min || n > max) { errors.push({ fieldId: field.id, message: `${field.label} must be between ${min} and ${max}.`, }); } } ``` **Step 2: Build to verify** Run: `node esbuild.config.mjs` Expected: No errors **Step 3: Commit** ```bash git add src/utils/validators.ts git commit -m "feat: add slider range validation" ``` --- ### Task 6: Update form builder for new field types **Files:** - Modify: `src/ui/form-builder.ts:4-15` (FIELD_TYPES array) - Modify: `src/ui/form-builder.ts:246-256` (placeholder condition) - Modify: `src/ui/form-builder.ts:283` (after note-link folder filter, add slider settings) **Step 1: Extend FIELD_TYPES array** Replace the `FIELD_TYPES` array: ```typescript const FIELD_TYPES: FieldType[] = [ 'text', 'textarea', 'number', 'toggle', 'date', 'dropdown', 'tags', 'note-link', 'folder-picker', 'rating', 'slider', 'color', 'time', ]; ``` **Step 2: Add time to placeholder-supporting types** Update the placeholder condition (line 246) to include `'time'`: ```typescript if (['text', 'textarea', 'number', 'date', 'time', 'note-link', 'folder-picker'].includes(field.type)) { ``` **Step 3: Add slider-specific settings** After the note-link folder filter block (after line 283), add: ```typescript // Min / Max / Step (for slider) if (field.type === 'slider') { this.addTextSetting( bodyEl, 'Min', String(field.min ?? 0), (v) => { field.min = v === '' ? undefined : Number(v); }, ); this.addTextSetting( bodyEl, 'Max', String(field.max ?? 100), (v) => { field.max = v === '' ? undefined : Number(v); }, ); this.addTextSetting( bodyEl, 'Step', String(field.step ?? 1), (v) => { field.step = v === '' ? undefined : Number(v); }, ); } ``` **Step 4: Build to verify** Run: `node esbuild.config.mjs` Expected: No errors **Step 5: Commit** ```bash git add src/ui/form-builder.ts git commit -m "feat: add slider, color, time to form builder with slider settings" ``` --- ### Task 7: Add keyboard accessibility to toggle and rating **Files:** - Modify: `src/ui/field-renderers.ts` (renderToggle and renderRating functions) **Step 1: Add keyboard support to toggle** In `renderToggle`, after creating the toggle div (line 135-136), add: ```typescript toggle.setAttribute('tabindex', '0'); toggle.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); enabled = !enabled; toggle.toggleClass('is-enabled', enabled); } }); ``` **Step 2: Add keyboard support to rating** In `renderRating`, after the star creation loop (after line 432), add `tabindex` and keyboard handling: ```typescript ratingEl.setAttribute('tabindex', '0'); ratingEl.addEventListener('keydown', (e: KeyboardEvent) => { if (e.key === 'ArrowRight' || e.key === 'ArrowUp') { e.preventDefault(); rating = Math.min(5, rating + 1); updateStars(); } else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') { e.preventDefault(); rating = Math.max(1, rating - 1); updateStars(); } }); ``` **Step 3: Build to verify** Run: `node esbuild.config.mjs` Expected: No errors **Step 4: Commit** ```bash git add src/ui/field-renderers.ts git commit -m "feat: add keyboard accessibility to toggle and rating fields" ``` --- ### Task 8: Add keyboard navigation to form modal **Files:** - Modify: `src/ui/form-modal.ts` (renderForm method) **Step 1: Add keydown listener to form modal** In the `renderForm` method, after the submit button event listener (after line 134), add: ```typescript // Keyboard navigation contentEl.addEventListener('keydown', (e: KeyboardEvent) => { // Ctrl+Enter always submits if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); this.handleSubmit(); return; } // Enter on single-line inputs submits (unless in suggest dropdown) if (e.key === 'Enter' && !e.shiftKey) { const target = e.target as HTMLElement; if ( target instanceof HTMLInputElement && target.type !== 'textarea' && !target.closest('.ff-suggest-wrapper') ) { e.preventDefault(); this.handleSubmit(); } } }); ``` **Step 2: Build to verify** Run: `node esbuild.config.mjs` Expected: No errors **Step 3: Commit** ```bash git add src/ui/form-modal.ts git commit -m "feat: add Ctrl+Enter and Enter keyboard navigation to form modal" ``` --- ### Task 9: Create form-io module **Files:** - Create: `src/utils/form-io.ts` **Step 1: Create the form-io module** Create `src/utils/form-io.ts`: ```typescript import { FormDefinition } from '../types'; interface FormExport { version: 1; forms: FormDefinition[]; } /** * Serialize forms to a JSON string with a version wrapper. */ export function exportForms(forms: FormDefinition[]): string { const payload: FormExport = { version: 1, forms }; return JSON.stringify(payload, null, 2); } /** * Parse a JSON string and extract valid FormDefinitions. * Generates new UUIDs for each imported form to prevent ID collisions. * Throws on invalid input. */ export function importForms(json: string): FormDefinition[] { let parsed: unknown; try { parsed = JSON.parse(json); } catch { throw new Error('Invalid JSON file'); } if ( typeof parsed !== 'object' || parsed === null || !('forms' in parsed) || !Array.isArray((parsed as FormExport).forms) ) { throw new Error('Invalid form format: missing "forms" array'); } const rawForms = (parsed as FormExport).forms; for (const form of rawForms) { if (!form.name || !Array.isArray(form.fields)) { throw new Error( 'Invalid form format: each form needs "name" and "fields"', ); } } // Generate fresh IDs to avoid collisions return rawForms.map((form) => ({ ...form, id: crypto.randomUUID(), name: form.name, })); } ``` **Step 2: Build to verify** Run: `node esbuild.config.mjs` Expected: No errors **Step 3: Commit** ```bash git add src/utils/form-io.ts git commit -m "feat: add form-io module for JSON export/import" ``` --- ### Task 10: Add import/export to settings tab **Files:** - Modify: `src/ui/settings-tab.ts` **Step 1: Add import for form-io** Add to the imports at the top of `settings-tab.ts`: ```typescript import { exportForms, importForms } from '../utils/form-io'; ``` **Step 2: Add Export All and Import buttons** In the `display()` method, after the "New Form" Setting block (after line 47), add: ```typescript // Import / Export new Setting(containerEl) .setName('Import / Export') .setDesc('Share forms as JSON files.') .addButton((btn) => { btn.setButtonText('Export All').onClick(() => { const forms = this.pluginRef.store.getAll(); if (forms.length === 0) { new Notice('No forms to export.'); return; } const json = exportForms(forms); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'formfire-export.json'; a.click(); URL.revokeObjectURL(url); }); }) .addButton((btn) => { btn.setButtonText('Import').onClick(() => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.addEventListener('change', async () => { const file = input.files?.[0]; if (!file) return; try { const text = await file.text(); const forms = importForms(text); for (const form of forms) { this.pluginRef.store.add(form); } await this.pluginRef.saveSettings(); this.pluginRef.refreshSidebar(); this.display(); new Notice(`Imported ${forms.length} form(s).`); } catch (err) { new Notice( err instanceof Error ? err.message : 'Import failed.', ); } }); input.click(); }); }); ``` **Step 3: Add per-form Export button** In the form list loop, add an Export button before the existing Edit button (line 73): ```typescript .addButton((btn) => { btn.setButtonText('Export').onClick(() => { const json = exportForms([form]); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `formfire-${form.name.toLowerCase().replace(/\s+/g, '-')}.json`; a.click(); URL.revokeObjectURL(url); }); }) ``` **Step 4: Build to verify** Run: `node esbuild.config.mjs` Expected: No errors **Step 5: Commit** ```bash git add src/ui/settings-tab.ts git commit -m "feat: add form import/export buttons to settings tab" ``` --- ### Task 11: Add styles for new field types **Files:** - Modify: `styles.css` **Step 1: Add slider styles** Add after the Rating Stars section (after line 301): ```css /* ========================================================================== Slider ========================================================================== */ .ff-slider-wrapper { display: flex; align-items: center; gap: 12px; } .ff-slider { flex: 1; height: 6px; -webkit-appearance: none; appearance: none; background: var(--background-modifier-border); border-radius: 3px; outline: none; cursor: pointer; } .ff-slider::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--interactive-accent); cursor: pointer; transition: box-shadow 0.15s ease; } .ff-slider::-webkit-slider-thumb:hover { box-shadow: 0 0 0 4px color-mix(in srgb, var(--interactive-accent) 20%, transparent); } .ff-slider::-moz-range-thumb { width: 18px; height: 18px; border: none; border-radius: 50%; background: var(--interactive-accent); cursor: pointer; } .ff-slider-value { min-width: 36px; text-align: right; font-size: 0.85rem; font-weight: 500; color: var(--text-normal); font-variant-numeric: tabular-nums; } /* ========================================================================== Color Picker ========================================================================== */ .ff-color-wrapper { display: flex; align-items: center; gap: 10px; } .ff-color-input { width: 40px; height: 32px; padding: 2px; border: 1px solid var(--background-modifier-border); border-radius: 4px; background: var(--background-primary); cursor: pointer; } .ff-color-input::-webkit-color-swatch-wrapper { padding: 2px; } .ff-color-input::-webkit-color-swatch { border: none; border-radius: 2px; } .ff-color-hex { font-size: 0.85rem; font-family: var(--font-monospace); color: var(--text-muted); letter-spacing: 0.03em; } ``` **Step 2: Build to verify** Run: `node esbuild.config.mjs` Expected: No errors (CSS isn't compiled by esbuild, but verifying no TS broke) **Step 3: Commit** ```bash git add styles.css git commit -m "feat: add styles for slider and color picker fields" ``` --- ### Task 12: 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 (all changes committed) **Step 3: Verify commit history** Run: `git log --oneline -12` Expected: 11 new commits on top of existing history