diff --git a/project-docs/alpine-migration-plan.md b/project-docs/alpine-migration-plan.md
index 7c43929..7ddfe9f 100644
--- a/project-docs/alpine-migration-plan.md
+++ b/project-docs/alpine-migration-plan.md
@@ -11,6 +11,7 @@ Incremental migration of Astrolabe from vanilla JavaScript to Alpine.js for reac
3. **Test after each step** - Catch issues early
4. **Alpine + Vanilla coexist** - No forced conversions
5. **SnippetStorage/DatasetStorage remain authoritative** - Alpine is view layer only
+6. **Migration only, no new features** - Convert existing functionality without adding new UI features
## Architecture Philosophy
@@ -36,8 +37,6 @@ Incremental migration of Astrolabe from vanilla JavaScript to Alpine.js for reac
## Phase 1: Snippet Panel ✅ COMPLETE
**Status**: Done
-**Effort**: 3 hours
-**Lines saved**: ~80 lines
**Files**: `index.html`, `src/js/snippet-manager.js`, `src/js/app.js`
### What Was Converted
@@ -52,1033 +51,204 @@ Incremental migration of Astrolabe from vanilla JavaScript to Alpine.js for reac
- SnippetStorage (localStorage operations)
- Editor integration
-- Meta fields (name, comment) - for now
+- Meta fields (name, comment)
- All CRUD business logic
### Key Learnings
- Alpine store keeps minimal UI state (`currentSnippetId`)
- Storage layer does all filtering/sorting
-- Alpine component is thin wrapper (~60 lines)
+- Alpine component is thin wrapper
- Automatic reactivity eliminates manual DOM updates
---
-## Phase 2: Dataset Manager Modal
+## Phase 2: Dataset Manager Modal ✅ COMPLETE
-**Status**: Planned
-**Effort**: 2-3 hours
-**Risk**: Low (modal is self-contained)
-**Lines to save**: ~100 lines
+**Status**: Done
**Files**: `src/js/dataset-manager.js`, `index.html`
-### What to Convert
+### What Was Converted
-**Target sections**:
-1. Dataset list rendering
-2. Search/filter controls
-3. Sort controls (name, created, modified, size)
-4. Selection highlighting
+- Dataset list rendering with `x-for` template
+- Selection highlighting with `:class` binding to Alpine store
+- Empty state with `x-show`
+- Click handlers with `@click`
-### Implementation Steps
+**Note**: Dataset modal did NOT have sort/search controls before migration, so none were added.
-#### Step 2.1: Add Alpine store for dataset selection
+### Implementation Approach
-```javascript
-// In dataset-manager.js, top of file
-document.addEventListener('alpine:init', () => {
- Alpine.store('datasets', {
- currentDatasetId: null
- });
-});
-```
-
-#### Step 2.2: Create Alpine component
-
-```javascript
-function datasetList() {
- return {
- searchQuery: '',
- sortBy: AppSettings.get('datasetSortBy') || 'modified',
- sortOrder: AppSettings.get('datasetSortOrder') || 'desc',
-
- get filteredDatasets() {
- return DatasetStorage.listDatasets(
- this.sortBy,
- this.sortOrder,
- this.searchQuery
- );
- },
-
- toggleSort(sortType) {
- if (this.sortBy === sortType) {
- this.sortOrder = this.sortOrder === 'desc' ? 'asc' : 'desc';
- } else {
- this.sortBy = sortType;
- this.sortOrder = 'desc';
- }
- AppSettings.set('datasetSortBy', this.sortBy);
- AppSettings.set('datasetSortOrder', this.sortOrder);
- },
-
- clearSearch() {
- this.searchQuery = '';
- const searchInput = document.getElementById('dataset-search');
- if (searchInput) searchInput.focus();
- },
-
- formatDate(dataset) {
- const date = this.sortBy === 'created' ? dataset.created : dataset.modified;
- return formatDatasetDate(date);
- },
-
- getSize(dataset) {
- return formatDatasetSize(dataset.size);
- },
-
- selectDataset(datasetId) {
- window.selectDataset(datasetId);
- },
-
- createNewDataset() {
- window.openDatasetImport();
- }
- };
-}
-```
-
-#### Step 2.3: Convert dataset list HTML
-
-Find the dataset list section in the modal and convert to Alpine:
-
-```html
-
-
-
- Sort by:
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
+ Import Dataset
- Click to import
-
-
-
-
- -
-
-
-
-
-
-
-
- No datasets found
-
-
-```
-
-#### Step 2.4: Update helper functions
-
-Convert these to no-ops:
-
-```javascript
-function renderDatasetList(searchQuery = null) {
- // Alpine.js handles rendering automatically
-}
-
-function initializeDatasetSortControls() {
- // Alpine.js handles this
-}
-
-function initializeDatasetSearchControls() {
- // Alpine.js handles this
-}
-```
-
-Update `selectDataset()`:
-
-```javascript
-function selectDataset(datasetId) {
- const dataset = await DatasetStorage.getDataset(datasetId);
- if (!dataset) return;
-
- // Update Alpine store selection
- if (typeof Alpine !== 'undefined' && Alpine.store('datasets')) {
- Alpine.store('datasets').currentDatasetId = datasetId;
- }
-
- // ... rest of function stays the same
-}
-```
+1. Add Alpine store with `currentDatasetId` for selection state
+2. Create `datasetList()` component as thin wrapper around existing logic
+3. Move meta formatting logic from inline HTML strings to component methods
+4. Convert HTML to use Alpine directives
+5. Update `renderDatasetList()` to trigger Alpine component refresh
+6. Update `selectDataset()` to update Alpine store instead of manual DOM manipulation
### What Stays Vanilla
- DatasetStorage (IndexedDB operations)
+- Dataset detail panel
- Preview table rendering
- Import/export logic
-- Data type detection
-- Format conversion
+- All dataset form/edit functionality
-### Validation Checklist
+### Key Learnings
-- [ ] Open dataset modal - list displays
-- [ ] Search filters datasets instantly
-- [ ] All 4 sort methods work (name, created, modified, size)
-- [ ] Sort direction toggles (⬇ ⬆)
-- [ ] Select dataset - highlights correctly
-- [ ] Import dataset - list updates
-- [ ] Delete dataset - list updates
-- [ ] Preview table renders correctly
-- [ ] Export functionality works
-- [ ] No console errors
+- Alpine component is very thin
+- Most logic moved from inline HTML strings to component methods
+- Net code increase: only +20 lines total
+- Same pattern as Phase 1: minimal, focused conversion
+- Don't add features during migration - only convert what exists
---
## Phase 3: View Mode Toggle (Draft/Published)
**Status**: Planned
-**Effort**: 30 minutes
-**Risk**: Very low (small isolated feature)
-**Lines to save**: ~20 lines
**Files**: `index.html`, `src/js/snippet-manager.js`
### What to Convert
-Current implementation uses manual class toggling. Replace with Alpine reactivity.
+View mode toggle buttons (Draft/Published) currently use manual class manipulation.
-### Implementation Steps
+### Implementation Approach
-#### Step 3.1: Add viewMode to Alpine store
+1. Add `viewMode` property to Alpine snippets store (default: 'draft')
+2. Convert button HTML to use `:class` binding and `@click` handlers
+3. Update `loadSnippetIntoEditor()` to read view mode from Alpine store
+4. Remove `updateViewModeUI()` function (no longer needed)
-```javascript
-// In snippet-manager.js, update Alpine store
-Alpine.store('snippets', {
- currentSnippetId: null,
- viewMode: 'draft' // Add this
-});
-```
+### What Stays Vanilla
-#### Step 3.2: Convert HTML buttons
-
-Find the view mode toggle buttons and add Alpine directives:
-
-```html
-
-
-
-
-```
-
-#### Step 3.3: Update editor loading logic
-
-```javascript
-function loadSnippetIntoEditor(snippet) {
- if (!editor || !snippet) return;
-
- window.isUpdatingEditor = true;
-
- // Get spec based on view mode from Alpine store
- const viewMode = (typeof Alpine !== 'undefined' && Alpine.store('snippets'))
- ? Alpine.store('snippets').viewMode
- : 'draft';
-
- const spec = viewMode === 'published' ? snippet.spec : snippet.draftSpec;
-
- // ... rest stays the same
-}
-```
-
-#### Step 3.4: Remove manual toggle code
-
-Find and simplify `updateViewModeUI()` function - no more manual classList manipulation needed.
-
-### Validation Checklist
-
-- [ ] Click "Draft" button - editor shows draft spec
-- [ ] Click "Published" button - editor shows published spec
-- [ ] Active button highlighted correctly
-- [ ] Switching snippets maintains current view mode
-- [ ] Publish/discard actions work correctly
+- Editor integration logic
+- Publish/discard actions
---
## Phase 4: Settings Modal
**Status**: Planned
-**Effort**: 1-2 hours
-**Risk**: Low (self-contained modal)
-**Lines to save**: ~50 lines
**Files**: `src/js/app.js`, `index.html`
### What to Convert
-Settings form with multiple inputs, apply/reset functionality.
+Settings form with multiple inputs and apply/reset functionality.
-### Implementation Steps
+### Implementation Approach
-#### Step 4.1: Create Alpine component
+1. Create `settingsPanel()` component with settings state tracking
+2. Use `x-model` for all form inputs (theme, editor settings, date format)
+3. Add computed `isDirty` property to enable/disable Apply button
+4. Use `x-show` for conditional fields (custom date format)
+5. Remove manual event listeners from app.js
-```javascript
-function settingsPanel() {
- return {
- settings: {},
- originalSettings: {},
+### What Stays Vanilla
- init() {
- this.loadSettings();
- },
-
- loadSettings() {
- this.settings = { ...AppSettings.getAll() };
- this.originalSettings = { ...this.settings };
- },
-
- get isDirty() {
- return JSON.stringify(this.settings) !== JSON.stringify(this.originalSettings);
- },
-
- applySettings() {
- // Update theme
- document.documentElement.setAttribute('data-theme', this.settings.ui.theme);
-
- // Update editor settings
- if (editor) {
- editor.updateOptions({
- fontSize: this.settings.editor.fontSize,
- theme: this.settings.editor.theme,
- minimap: { enabled: this.settings.editor.minimap },
- wordWrap: this.settings.editor.wordWrap,
- lineNumbers: this.settings.editor.lineNumbers,
- tabSize: this.settings.editor.tabSize
- });
- }
-
- // Save to localStorage
- AppSettings.setMultiple(this.settings);
-
- this.originalSettings = { ...this.settings };
-
- // Re-render snippet list to reflect date format changes
- renderSnippetList();
-
- // Update metadata display if snippet selected
- const currentSnippet = getCurrentSnippet();
- if (currentSnippet) {
- selectSnippet(currentSnippet.id, false);
- }
-
- Toast.success('Settings saved');
- },
-
- resetSettings() {
- this.settings = { ...AppSettings.defaults };
- },
-
- cancelSettings() {
- this.loadSettings();
- closeSettings();
- }
- };
-}
-```
-
-#### Step 4.2: Convert settings form HTML
-
-Use `x-model` for all form inputs:
-
-```html
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-#### Step 4.3: Simplify settings initialization
-
-Remove manual event listener registration in app.js - Alpine handles it all.
-
-### Validation Checklist
-
-- [ ] Settings modal opens with current values
-- [ ] All form inputs work (dropdowns, checkboxes, numbers)
-- [ ] Apply button disabled when no changes
-- [ ] Apply button enabled when settings change
-- [ ] Apply saves and applies settings immediately
-- [ ] Theme changes take effect
-- [ ] Editor settings apply to Monaco
-- [ ] Reset to Defaults works
-- [ ] Cancel discards changes
-- [ ] Custom date format field shows/hides correctly
+- AppSettings storage layer
+- Editor option updates
+- Theme application logic
---
## Phase 5: Chart Builder Modal
**Status**: Planned
-**Effort**: 2-3 hours
-**Risk**: Medium (more complex state)
-**Lines to save**: ~80 lines
**Files**: `src/js/chart-builder.js`, `index.html`
### What to Convert
Chart builder form with dataset selection, chart type, and field mappings.
-### Implementation Steps
+### Implementation Approach
-#### Step 5.1: Create Alpine component
-
-```javascript
-function chartBuilder() {
- return {
- chartType: 'bar',
- datasetId: null,
- dataset: null,
- encoding: {
- x: null,
- y: null,
- color: null,
- size: null
- },
-
- async init() {
- // Load datasets
- const datasets = await DatasetStorage.getAllDatasets();
- this.datasets = datasets;
- },
-
- get availableDatasets() {
- return this.datasets || [];
- },
-
- get availableFields() {
- if (!this.dataset) return [];
- return Object.keys(this.dataset.preview || {});
- },
-
- async selectDataset(datasetId) {
- this.datasetId = datasetId;
- this.dataset = await DatasetStorage.getDataset(datasetId);
- // Reset encodings when dataset changes
- this.encoding = { x: null, y: null, color: null, size: null };
- },
-
- get specPreview() {
- if (!this.datasetId) return null;
-
- const spec = {
- $schema: "https://vega.github.io/schema/vega-lite/v5.json",
- data: { name: this.dataset.name },
- mark: this.chartType,
- encoding: {}
- };
-
- if (this.encoding.x) spec.encoding.x = { field: this.encoding.x, type: "quantitative" };
- if (this.encoding.y) spec.encoding.y = { field: this.encoding.y, type: "quantitative" };
- if (this.encoding.color) spec.encoding.color = { field: this.encoding.color, type: "nominal" };
- if (this.encoding.size) spec.encoding.size = { field: this.encoding.size, type: "quantitative" };
-
- return JSON.stringify(spec, null, 2);
- },
-
- get canInsert() {
- return this.datasetId && this.encoding.x && this.encoding.y;
- },
-
- insertChart() {
- if (!this.canInsert) return;
-
- // Insert spec into editor
- if (editor) {
- window.isUpdatingEditor = true;
- editor.setValue(this.specPreview);
- window.isUpdatingEditor = false;
-
- autoSaveDraft();
- renderPreview();
- }
-
- closeChartBuilder();
- Toast.success('Chart inserted into editor');
- }
- };
-}
-```
-
-#### Step 5.2: Convert chart builder HTML
-
-```html
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
+1. Create `chartBuilder()` component with form state
+2. Load datasets on init, populate dropdowns with `x-for`
+3. Track selected dataset and available fields
+4. Generate spec preview with computed property
+5. Enable/disable insert button based on validation
### What Stays Vanilla
- Dataset field detection
- Type inference logic
-- Spec generation utilities (if complex)
-
-### Validation Checklist
-
-- [ ] Chart builder modal opens
-- [ ] Dataset dropdown populates
-- [ ] Select dataset - field dropdowns populate
-- [ ] Change chart type - preview updates
-- [ ] Select X/Y fields - preview updates
-- [ ] Optional fields (color, size) work
-- [ ] Insert button disabled when invalid
-- [ ] Insert button enabled when valid
-- [ ] Insert adds spec to editor
-- [ ] Cancel closes modal
+- Spec generation utilities
---
## Phase 6: Meta Fields (Name, Comment)
**Status**: Planned
-**Effort**: 1 hour
-**Risk**: Low
-**Lines to save**: ~30 lines
**Files**: `index.html`, `src/js/snippet-manager.js`
### What to Convert
Name and comment fields with auto-save functionality.
-### Implementation Steps
+### Implementation Approach
-#### Step 6.1: Add to snippetList component
+1. Add `snippetName` and `snippetComment` to `snippetList()` component
+2. Use `x-model` with debounced input handlers
+3. Call `loadMetadata()` when snippet selected
+4. Auto-save on change with 500ms debounce
-```javascript
-function snippetList() {
- return {
- // ... existing properties
+### What Stays Vanilla
- snippetName: '',
- snippetComment: '',
-
- loadMetadata(snippet) {
- this.snippetName = snippet.name || '';
- this.snippetComment = snippet.comment || '';
- },
-
- saveMetadata() {
- const snippet = getCurrentSnippet();
- if (!snippet) return;
-
- snippet.name = this.snippetName;
- snippet.comment = this.snippetComment;
-
- SnippetStorage.saveSnippet(snippet);
- renderSnippetList(); // Refresh list to show new name
- }
- };
-}
-```
-
-#### Step 6.2: Convert HTML inputs
-
-```html
-
-
-
-
-
-
-
-```
-
-#### Step 6.3: Update selectSnippet function
-
-Call `loadMetadata()` when snippet selected:
-
-```javascript
-function selectSnippet(snippetId, updateURL = true) {
- // ... existing code
-
- // Load metadata into Alpine component
- if (typeof Alpine !== 'undefined') {
- const component = Alpine.$data(document.querySelector('[x-data*="snippetList"]'));
- if (component) {
- component.loadMetadata(snippet);
- }
- }
-}
-```
-
-### Validation Checklist
-
-- [ ] Select snippet - name and comment populate
-- [ ] Edit name - auto-saves after 500ms
-- [ ] Edit comment - auto-saves after 500ms
-- [ ] Name changes reflect in snippet list
-- [ ] Switching snippets loads correct metadata
+- SnippetStorage save operations
---
## Phase 7: Panel Visibility Toggles
**Status**: Planned
-**Effort**: 30 minutes
-**Risk**: Very low
-**Lines to save**: ~20 lines
**Files**: `index.html`, `src/js/panel-manager.js`
### What to Convert
Toggle buttons for showing/hiding panels.
-### Implementation Steps
+### Implementation Approach
-#### Step 7.1: Create Alpine store for UI state
+1. Create Alpine store for UI state (panel visibility flags)
+2. Convert toggle buttons to use `:class` and `@click`
+3. Add `x-show` to panels with transitions
+4. Persist visibility state to localStorage
-```javascript
-// In panel-manager.js or app.js
-document.addEventListener('alpine:init', () => {
- Alpine.store('ui', {
- snippetPanelVisible: true,
- editorPanelVisible: true,
- previewPanelVisible: true
- });
-});
-```
+### What Stays Vanilla
-#### Step 7.2: Convert toggle buttons
-
-```html
-
-
-
-
-
-```
-
-#### Step 7.3: Update panel visibility
-
-```html
-
-
-
-
-
-
-
-
-
-
-
-```
-
-#### Step 7.4: Persist state
-
-```javascript
-// Add watchers in Alpine.init
-Alpine.effect(() => {
- const ui = Alpine.store('ui');
- localStorage.setItem('ui-panel-visibility', JSON.stringify({
- snippetPanel: ui.snippetPanelVisible,
- editorPanel: ui.editorPanelVisible,
- previewPanel: ui.previewPanelVisible
- }));
-});
-
-// Load on init
-const saved = localStorage.getItem('ui-panel-visibility');
-if (saved) {
- const { snippetPanel, editorPanel, previewPanel } = JSON.parse(saved);
- Alpine.store('ui').snippetPanelVisible = snippetPanel;
- Alpine.store('ui').editorPanelVisible = editorPanel;
- Alpine.store('ui').previewPanelVisible = previewPanel;
-}
-```
-
-### Validation Checklist
-
-- [ ] Toggle buttons show active state
-- [ ] Click toggles show/hide panel
-- [ ] Transitions are smooth
-- [ ] State persists on page refresh
-- [ ] Keyboard shortcuts still work
-- [ ] Resizing works with hidden panels
+- Panel resizing logic
+- Keyboard shortcuts
---
## Phase 8: Toast Notifications (Optional)
**Status**: Planned
-**Effort**: 1-2 hours
-**Risk**: Low
-**Lines to save**: ~40 lines
**Files**: `src/js/config.js`, `index.html`
### What to Convert
Toast notification system with auto-dismiss.
-### Implementation Steps
+### Implementation Approach
-#### Step 8.1: Create Alpine store
+1. Create Alpine store for toast queue
+2. Render toasts with `x-for` and transitions
+3. Update `Toast` utility to add items to Alpine store
+4. Auto-dismiss with setTimeout
-```javascript
-// In config.js
-document.addEventListener('alpine:init', () => {
- Alpine.store('toasts', {
- items: [],
- nextId: 1,
+### What Stays Vanilla
- show(message, type = 'info', duration = 3000) {
- const id = this.nextId++;
- this.items.push({ id, message, type });
-
- if (duration > 0) {
- setTimeout(() => this.dismiss(id), duration);
- }
- },
-
- dismiss(id) {
- this.items = this.items.filter(t => t.id !== id);
- },
-
- success(message, duration = 3000) {
- this.show(message, 'success', duration);
- },
-
- error(message, duration = 5000) {
- this.show(message, 'error', duration);
- },
-
- info(message, duration = 3000) {
- this.show(message, 'info', duration);
- },
-
- warning(message, duration = 4000) {
- this.show(message, 'warning', duration);
- }
- });
-});
-
-// Update Toast utility to use Alpine store
-const Toast = {
- success: (msg, duration) => Alpine.store('toasts').success(msg, duration),
- error: (msg, duration) => Alpine.store('toasts').error(msg, duration),
- info: (msg, duration) => Alpine.store('toasts').info(msg, duration),
- warning: (msg, duration) => Alpine.store('toasts').warning(msg, duration)
-};
-```
-
-#### Step 8.2: Convert HTML
-
-```html
-
-```
-
-#### Step 8.3: Add transition CSS
-
-```css
-.toast-enter {
- transition: all 0.3s ease-out;
-}
-.toast-enter-start {
- opacity: 0;
- transform: translateY(-1rem);
-}
-.toast-enter-end {
- opacity: 1;
- transform: translateY(0);
-}
-.toast-leave {
- transition: all 0.2s ease-in;
-}
-.toast-leave-start {
- opacity: 1;
- transform: translateY(0);
-}
-.toast-leave-end {
- opacity: 0;
- transform: translateY(-1rem);
-}
-```
-
-### Validation Checklist
-
-- [ ] Success toast appears and auto-dismisses
-- [ ] Error toast appears and auto-dismisses
-- [ ] Multiple toasts stack correctly
-- [ ] Click to dismiss works
-- [ ] Animations are smooth
-- [ ] No memory leaks from timers
-
----
-
-## Migration Timeline Estimate
-
-| Phase | Effort | Cumulative Lines Saved | Risk Level |
-|-------|--------|------------------------|------------|
-| 1. Snippet Panel ✅ | 3h | -80 | Low |
-| 2. Dataset Manager | 3h | -180 | Low |
-| 3. View Mode Toggle | 0.5h | -200 | Very Low |
-| 4. Settings Modal | 2h | -250 | Low |
-| 5. Chart Builder | 3h | -330 | Medium |
-| 6. Meta Fields | 1h | -360 | Low |
-| 7. Panel Toggles | 0.5h | -380 | Very Low |
-| 8. Toast Notifications | 2h | -420 | Low |
-| **Total** | **~15 hours** | **~420 lines** | |
-
----
-
-## Testing Checklist Template
-
-Use this after completing each phase:
-
-### Core Functionality
-- [ ] Page loads without errors
-- [ ] All existing features work
-- [ ] No console errors
-- [ ] Performance is good (no lag)
-
-### Converted Feature
-- [ ] Reactivity works (search, sort, filters)
-- [ ] User interactions respond correctly
-- [ ] State persists correctly
-- [ ] Edge cases handled (empty states, errors)
-- [ ] Animations/transitions smooth
-
-### Regression Testing
-- [ ] Other features still work
-- [ ] Import/export still functional
-- [ ] Keyboard shortcuts still work
-- [ ] Browser refresh preserves state
-- [ ] Multiple tabs/windows work correctly
-
-### Browser Compatibility
-- [ ] Chrome/Edge works
-- [ ] Firefox works
-- [ ] Safari works (if available)
-
----
-
-## Emergency Rollback Plan
-
-If a phase causes issues:
-
-### Option 1: Quick Fix
-1. Comment out Alpine directives in HTML
-2. Uncomment vanilla JavaScript code
-3. Test that functionality is restored
-
-### Option 2: Git Revert
-Each phase should be a separate commit:
-```bash
-git log --oneline # Find the commit
-git revert
-```
-
-### Option 3: Hybrid Mode
-Alpine and vanilla can coexist:
-- Keep working Alpine sections
-- Revert problematic sections to vanilla
-- Fix issues in separate branch
-- Merge when stable
+- Toast message generation
---
## Recommended Order
-Based on risk/reward analysis:
-
1. ✅ **Phase 1: Snippet Panel** - DONE
-2. **Phase 2: Dataset Manager** - Similar complexity, good practice
-3. **Phase 3: View Mode Toggle** - Quick win, validates pattern
+2. ✅ **Phase 2: Dataset Manager** - DONE
+3. **Phase 3: View Mode Toggle** - Quick win
4. **Phase 4: Settings Modal** - Another modal, builds confidence
5. **Phase 6: Meta Fields** - Before Chart Builder (simpler)
6. **Phase 7: Panel Toggles** - Quick win
@@ -1087,68 +257,55 @@ Based on risk/reward analysis:
---
-## Post-Migration Considerations
+## Emergency Rollback Plan
+
+If a phase causes issues:
+
+1. **Quick Fix**: Comment out Alpine directives, uncomment vanilla JS
+2. **Git Revert**: Each phase is a separate commit
+3. **Hybrid Mode**: Alpine and vanilla can coexist - revert problematic sections only
+
+---
+
+## Post-Migration
After all phases complete:
### Code Cleanup
-- Remove all no-op functions
+- Remove no-op functions
- Remove unused vanilla event listeners
- Clean up global state variables
- Update JSDoc comments
-### Documentation Updates
+### Documentation
- Update architecture.md
- Document Alpine components
- Add Alpine.js to dependencies list
- Update CLAUDE.md with Alpine patterns
-### Performance Optimization
-- Monitor Alpine reactivity performance
-- Add `x-cloak` for flicker prevention
-- Consider Alpine.js plugins if needed
-
-### Future Enhancements
-With Alpine in place, new features become easier:
-- Drag-and-drop reordering
-- Inline editing
-- Real-time collaboration indicators
-- Advanced filtering (tags, date ranges)
-
---
## Questions & Decisions Log
-Track decisions made during migration:
-
| Date | Phase | Question | Decision | Rationale |
|------|-------|----------|----------|-----------|
| 2025-01-24 | 1 | Store snippets in Alpine or Storage? | Storage | Single source of truth, Alpine just views |
| 2025-01-24 | 1 | Keep old functions as stubs? | Yes | Backwards compatibility, easier rollback |
+| 2025-01-24 | 2 | Add sort/search to dataset modal? | No | Migration only - don't add features that didn't exist |
+| 2025-01-24 | 2 | How much net code increase is acceptable? | ~20 lines | Alpine boilerplate worth it for reactivity gains |
---
## Success Metrics
-How we'll know the migration is successful:
-
### Quantitative
-- [ ] ~400+ lines of code removed
-- [ ] No performance regression (< 5% slower)
-- [ ] Zero increase in bug reports
-- [ ] All tests passing
+- ~300+ lines of code removed overall
+- No performance regression
+- Zero increase in bug reports
+- All tests passing
### Qualitative
-- [ ] Code is more readable
-- [ ] New features easier to add
-- [ ] Less manual DOM manipulation
-- [ ] Clearer separation of concerns
-
----
-
-## Resources
-
-- [Alpine.js Documentation](https://alpinejs.dev/)
-- [Alpine.js Examples](https://alpinejs.dev/examples)
-- [Alpine.js Cheatsheet](https://alpinejs.dev/cheatsheet)
-- Project: `/project-docs/architecture.md`
+- Code is more readable
+- New features easier to add
+- Less manual DOM manipulation
+- Clearer separation of concerns
diff --git a/src/js/dataset-manager.js b/src/js/dataset-manager.js
index 34919ac..44696c5 100644
--- a/src/js/dataset-manager.js
+++ b/src/js/dataset-manager.js
@@ -1,5 +1,50 @@
// Dataset management with IndexedDB
+// Alpine.js store for dataset UI state
+document.addEventListener('alpine:init', () => {
+ Alpine.store('datasets', {
+ currentDatasetId: null
+ });
+});
+
+// Alpine.js component for dataset list - thin wrapper around existing logic
+function datasetList() {
+ return {
+ datasets: [],
+
+ async init() {
+ await this.loadDatasets();
+ },
+
+ async loadDatasets() {
+ this.datasets = await DatasetStorage.listDatasets();
+ // Sort by modified date (most recent first) - keeping existing behavior
+ this.datasets.sort((a, b) => new Date(b.modified) - new Date(a.modified));
+ },
+
+ formatMeta(dataset) {
+ const formatLabel = dataset.format ? dataset.format.toUpperCase() : 'UNKNOWN';
+ if (dataset.source === 'url') {
+ if (dataset.rowCount !== null && dataset.size !== null) {
+ return `URL • ${dataset.rowCount} rows • ${formatLabel} • ${formatBytes(dataset.size)}`;
+ } else {
+ return `URL • ${formatLabel}`;
+ }
+ } else {
+ return `${dataset.rowCount} rows • ${formatLabel} • ${formatBytes(dataset.size)}`;
+ }
+ },
+
+ getUsageCount(dataset) {
+ return countSnippetUsage(dataset.name);
+ },
+
+ selectDataset(datasetId) {
+ window.selectDataset(datasetId);
+ }
+ };
+}
+
const DB_NAME = 'astrolabe-datasets';
const DB_VERSION = 1;
const STORE_NAME = 'datasets';
@@ -348,55 +393,15 @@ async function fetchURLMetadata(url, format) {
}
// Render dataset list in modal
+// Alpine.js now handles rendering, this just triggers a refresh
async function renderDatasetList() {
- const datasets = await DatasetStorage.listDatasets();
-
- if (datasets.length === 0) {
- document.getElementById('dataset-list').innerHTML = 'No datasets yet. Click "New Dataset" to create one.
';
- return;
- }
-
- // Sort by modified date (most recent first)
- datasets.sort((a, b) => new Date(b.modified) - new Date(a.modified));
-
- // Format individual dataset items
- const formatDatasetItem = (dataset) => {
- let metaText;
- const formatLabel = dataset.format ? dataset.format.toUpperCase() : 'UNKNOWN';
-
- if (dataset.source === 'url') {
- // Show metadata if available, otherwise just URL and format
- if (dataset.rowCount !== null && dataset.size !== null) {
- metaText = `URL • ${dataset.rowCount} rows • ${formatLabel} • ${formatBytes(dataset.size)}`;
- } else {
- metaText = `URL • ${formatLabel}`;
- }
- } else {
- metaText = `${dataset.rowCount} rows • ${formatLabel} • ${formatBytes(dataset.size)}`;
+ const listView = document.getElementById('dataset-list-view');
+ if (listView && listView.__x) {
+ const component = Alpine.$data(listView);
+ if (component && component.loadDatasets) {
+ await component.loadDatasets();
}
-
- // Count snippet usage and create badge
- const usageCount = countSnippetUsage(dataset.name);
- const usageBadge = usageCount > 0
- ? `📄 ${usageCount}
`
- : '';
-
- return `
-
-
-
${dataset.name}
-
${metaText}
-
- ${usageBadge}
-
- `;
- };
-
- // Use generic list renderer
- renderGenericList('dataset-list', datasets, formatDatasetItem, selectDataset, {
- emptyMessage: 'No datasets yet. Click "New Dataset" to create one.',
- itemSelector: '.dataset-item'
- });
+ }
}
// Select a dataset and show details
@@ -404,13 +409,9 @@ async function selectDataset(datasetId, updateURL = true) {
const dataset = await DatasetStorage.getDataset(datasetId);
if (!dataset) return;
- // Update selection state
- document.querySelectorAll('.dataset-item').forEach(item => {
- item.classList.remove('selected');
- });
- const selectedItem = document.querySelector(`[data-item-id="${datasetId}"]`);
- if (selectedItem) {
- selectedItem.classList.add('selected');
+ // Update Alpine store selection (Alpine handles highlighting via :class binding)
+ if (typeof Alpine !== 'undefined' && Alpine.store('datasets')) {
+ Alpine.store('datasets').currentDatasetId = datasetId;
}
// Show details panel