docs: add Phase 1 implementation plan
12 tasks covering new field types, keyboard navigation, and form import/export. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d48bb1b052
commit
de5ea170e1
1 changed files with 795 additions and 0 deletions
795
docs/plans/2026-02-13-phase1-implementation.md
Normal file
795
docs/plans/2026-02-13-phase1-implementation.md
Normal file
|
|
@ -0,0 +1,795 @@
|
||||||
|
# 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
|
||||||
Loading…
Reference in a new issue