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