Initial release: Ironpad v0.1.0 - Local-first, file-based project and knowledge management system. Rust backend, Vue 3 frontend, Milkdown editor, Git integration, cross-platform builds. Built with AI using Open Method.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
218
frontend/src/stores/git.ts
Normal file
218
frontend/src/stores/git.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { GitStatus, CommitInfo, CommitDetail, DiffInfo, RemoteInfo } from '../types'
|
||||
import { gitApi } from '../api/client'
|
||||
|
||||
export const useGitStore = defineStore('git', () => {
|
||||
// State
|
||||
const status = ref<GitStatus | null>(null)
|
||||
const loading = ref(false)
|
||||
const committing = ref(false)
|
||||
const pushing = ref(false)
|
||||
const fetching = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const conflicts = ref<string[]>([])
|
||||
|
||||
// New state for expanded git features
|
||||
const history = ref<CommitDetail[]>([])
|
||||
const historyLoading = ref(false)
|
||||
const workingDiff = ref<DiffInfo | null>(null)
|
||||
const diffLoading = ref(false)
|
||||
const selectedCommitDiff = ref<DiffInfo | null>(null)
|
||||
const selectedCommitId = ref<string | null>(null)
|
||||
const remote = ref<RemoteInfo | null>(null)
|
||||
const panelOpen = ref(false)
|
||||
|
||||
// Getters
|
||||
const hasChanges = computed(() => status.value?.has_changes ?? false)
|
||||
const hasConflicts = computed(() => conflicts.value.length > 0)
|
||||
const branch = computed(() => status.value?.branch ?? 'main')
|
||||
const isRepo = computed(() => status.value?.is_repo ?? false)
|
||||
const changedFilesCount = computed(() => status.value?.files.length ?? 0)
|
||||
const hasRemote = computed(() => remote.value !== null)
|
||||
const canPush = computed(() => (remote.value?.ahead ?? 0) > 0)
|
||||
const canPull = computed(() => (remote.value?.behind ?? 0) > 0)
|
||||
|
||||
// Actions
|
||||
async function loadStatus() {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
status.value = await gitApi.status()
|
||||
} catch (err) {
|
||||
error.value = `Failed to load git status: ${err}`
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function commit(message?: string): Promise<CommitInfo | null> {
|
||||
try {
|
||||
committing.value = true
|
||||
error.value = null
|
||||
const result = await gitApi.commit(message)
|
||||
await loadStatus()
|
||||
// Refresh history and diff after commit
|
||||
await Promise.all([loadHistory(), loadWorkingDiff(), loadRemote()])
|
||||
return result
|
||||
} catch (err) {
|
||||
error.value = `Commit failed: ${err}`
|
||||
return null
|
||||
} finally {
|
||||
committing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function push() {
|
||||
try {
|
||||
pushing.value = true
|
||||
error.value = null
|
||||
const result = await gitApi.push()
|
||||
if (!result.success) {
|
||||
throw new Error(result.message)
|
||||
}
|
||||
await Promise.all([loadStatus(), loadRemote()])
|
||||
} catch (err) {
|
||||
error.value = `Push failed: ${err}`
|
||||
throw err
|
||||
} finally {
|
||||
pushing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchRemote() {
|
||||
try {
|
||||
fetching.value = true
|
||||
error.value = null
|
||||
const result = await gitApi.fetch()
|
||||
if (!result.success) {
|
||||
throw new Error(result.message)
|
||||
}
|
||||
await loadRemote()
|
||||
} catch (err) {
|
||||
error.value = `Fetch failed: ${err}`
|
||||
throw err
|
||||
} finally {
|
||||
fetching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function checkConflicts() {
|
||||
try {
|
||||
error.value = null
|
||||
conflicts.value = await gitApi.conflicts()
|
||||
} catch (err) {
|
||||
// Conflicts endpoint might not exist yet, ignore error
|
||||
conflicts.value = []
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistory(limit?: number) {
|
||||
try {
|
||||
historyLoading.value = true
|
||||
history.value = await gitApi.log(limit)
|
||||
} catch (err) {
|
||||
console.error('Failed to load git history:', err)
|
||||
history.value = []
|
||||
} finally {
|
||||
historyLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadWorkingDiff() {
|
||||
try {
|
||||
diffLoading.value = true
|
||||
workingDiff.value = await gitApi.diff()
|
||||
} catch (err) {
|
||||
console.error('Failed to load working diff:', err)
|
||||
workingDiff.value = null
|
||||
} finally {
|
||||
diffLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCommitDiff(commitId: string) {
|
||||
try {
|
||||
diffLoading.value = true
|
||||
selectedCommitId.value = commitId
|
||||
selectedCommitDiff.value = await gitApi.commitDiff(commitId)
|
||||
} catch (err) {
|
||||
console.error('Failed to load commit diff:', err)
|
||||
selectedCommitDiff.value = null
|
||||
} finally {
|
||||
diffLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadRemote() {
|
||||
try {
|
||||
remote.value = await gitApi.remote()
|
||||
} catch (err) {
|
||||
console.error('Failed to load remote info:', err)
|
||||
remote.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function clearSelectedCommit() {
|
||||
selectedCommitId.value = null
|
||||
selectedCommitDiff.value = null
|
||||
}
|
||||
|
||||
function togglePanel() {
|
||||
panelOpen.value = !panelOpen.value
|
||||
if (panelOpen.value) {
|
||||
// Load all data when opening panel
|
||||
Promise.all([
|
||||
loadStatus(),
|
||||
loadHistory(),
|
||||
loadWorkingDiff(),
|
||||
loadRemote()
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
function clearError() {
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
status,
|
||||
loading,
|
||||
committing,
|
||||
pushing,
|
||||
fetching,
|
||||
error,
|
||||
conflicts,
|
||||
history,
|
||||
historyLoading,
|
||||
workingDiff,
|
||||
diffLoading,
|
||||
selectedCommitDiff,
|
||||
selectedCommitId,
|
||||
remote,
|
||||
panelOpen,
|
||||
// Getters
|
||||
hasChanges,
|
||||
hasConflicts,
|
||||
branch,
|
||||
isRepo,
|
||||
changedFilesCount,
|
||||
hasRemote,
|
||||
canPush,
|
||||
canPull,
|
||||
// Actions
|
||||
loadStatus,
|
||||
commit,
|
||||
push,
|
||||
fetchRemote,
|
||||
checkConflicts,
|
||||
loadHistory,
|
||||
loadWorkingDiff,
|
||||
loadCommitDiff,
|
||||
loadRemote,
|
||||
clearSelectedCommit,
|
||||
togglePanel,
|
||||
clearError
|
||||
}
|
||||
})
|
||||
8
frontend/src/stores/index.ts
Normal file
8
frontend/src/stores/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export { useNotesStore } from './notes'
|
||||
export { useProjectsStore } from './projects'
|
||||
export { useTasksStore } from './tasks'
|
||||
export { useUiStore } from './ui'
|
||||
export { useWebSocketStore } from './websocket'
|
||||
export { useGitStore } from './git'
|
||||
export { useWorkspaceStore } from './workspace'
|
||||
export { useThemeStore } from './theme'
|
||||
131
frontend/src/stores/notes.ts
Normal file
131
frontend/src/stores/notes.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Note, NoteSummary } from '../types'
|
||||
import { notesApi } from '../api/client'
|
||||
|
||||
export const useNotesStore = defineStore('notes', () => {
|
||||
// State
|
||||
const notes = ref<NoteSummary[]>([])
|
||||
const currentNote = ref<Note | null>(null)
|
||||
const loading = ref(false)
|
||||
const loadingNote = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
const saveStatus = ref<'idle' | 'saving' | 'saved' | 'error'>('idle')
|
||||
|
||||
// Getters
|
||||
const sortedNotes = computed(() =>
|
||||
[...notes.value].sort((a, b) => {
|
||||
const dateA = a.updated ? new Date(a.updated).getTime() : 0
|
||||
const dateB = b.updated ? new Date(b.updated).getTime() : 0
|
||||
return dateB - dateA
|
||||
})
|
||||
)
|
||||
|
||||
const getNoteById = computed(() => (id: string) =>
|
||||
notes.value.find(n => n.id === id)
|
||||
)
|
||||
|
||||
// Actions
|
||||
async function loadNotes() {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
notes.value = await notesApi.list()
|
||||
} catch (err) {
|
||||
error.value = `Failed to load notes: ${err}`
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadNote(id: string) {
|
||||
try {
|
||||
loadingNote.value = true
|
||||
error.value = null
|
||||
currentNote.value = await notesApi.get(id)
|
||||
saveStatus.value = 'idle'
|
||||
} catch (err) {
|
||||
error.value = `Failed to load note: ${err}`
|
||||
currentNote.value = null
|
||||
} finally {
|
||||
loadingNote.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createNote() {
|
||||
try {
|
||||
error.value = null
|
||||
const newNote = await notesApi.create()
|
||||
await loadNotes()
|
||||
return newNote
|
||||
} catch (err) {
|
||||
error.value = `Failed to create note: ${err}`
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function saveNote(content: string) {
|
||||
if (!currentNote.value) return
|
||||
|
||||
try {
|
||||
saveStatus.value = 'saving'
|
||||
currentNote.value = await notesApi.update(currentNote.value.id, content)
|
||||
saveStatus.value = 'saved'
|
||||
await loadNotes() // Refresh list to update timestamps
|
||||
setTimeout(() => {
|
||||
if (saveStatus.value === 'saved') saveStatus.value = 'idle'
|
||||
}, 2000)
|
||||
} catch (err) {
|
||||
saveStatus.value = 'error'
|
||||
error.value = `Failed to save note: ${err}`
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteNote(id?: string) {
|
||||
const noteId = id || currentNote.value?.id
|
||||
if (!noteId) return
|
||||
|
||||
try {
|
||||
error.value = null
|
||||
await notesApi.delete(noteId)
|
||||
if (currentNote.value?.id === noteId) {
|
||||
currentNote.value = null
|
||||
}
|
||||
await loadNotes()
|
||||
} catch (err) {
|
||||
error.value = `Failed to archive note: ${err}`
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function clearCurrentNote() {
|
||||
currentNote.value = null
|
||||
saveStatus.value = 'idle'
|
||||
}
|
||||
|
||||
function clearError() {
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
notes,
|
||||
currentNote,
|
||||
loading,
|
||||
loadingNote,
|
||||
error,
|
||||
saveStatus,
|
||||
// Getters
|
||||
sortedNotes,
|
||||
getNoteById,
|
||||
// Actions
|
||||
loadNotes,
|
||||
loadNote,
|
||||
createNote,
|
||||
saveNote,
|
||||
deleteNote,
|
||||
clearCurrentNote,
|
||||
clearError
|
||||
}
|
||||
})
|
||||
84
frontend/src/stores/projects.ts
Normal file
84
frontend/src/stores/projects.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Project } from '../types'
|
||||
import { projectsApi } from '../api/client'
|
||||
|
||||
export const useProjectsStore = defineStore('projects', () => {
|
||||
// State
|
||||
const projects = ref<Project[]>([])
|
||||
const currentProject = ref<Project | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Getters
|
||||
const sortedProjects = computed(() =>
|
||||
[...projects.value].sort((a, b) => a.name.localeCompare(b.name))
|
||||
)
|
||||
|
||||
const getProjectById = computed(() => (id: string) =>
|
||||
projects.value.find(p => p.id === id)
|
||||
)
|
||||
|
||||
// Actions
|
||||
async function loadProjects() {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
projects.value = await projectsApi.list()
|
||||
} catch (err) {
|
||||
error.value = `Failed to load projects: ${err}`
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProject(id: string) {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
currentProject.value = await projectsApi.get(id)
|
||||
} catch (err) {
|
||||
error.value = `Failed to load project: ${err}`
|
||||
currentProject.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function createProject(name: string) {
|
||||
try {
|
||||
error.value = null
|
||||
const newProject = await projectsApi.create(name)
|
||||
await loadProjects()
|
||||
return newProject
|
||||
} catch (err) {
|
||||
error.value = `Failed to create project: ${err}`
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function clearCurrentProject() {
|
||||
currentProject.value = null
|
||||
}
|
||||
|
||||
function clearError() {
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
projects,
|
||||
currentProject,
|
||||
loading,
|
||||
error,
|
||||
// Getters
|
||||
sortedProjects,
|
||||
getProjectById,
|
||||
// Actions
|
||||
loadProjects,
|
||||
loadProject,
|
||||
createProject,
|
||||
clearCurrentProject,
|
||||
clearError
|
||||
}
|
||||
})
|
||||
235
frontend/src/stores/tasks.ts
Normal file
235
frontend/src/stores/tasks.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Task, TaskWithContent } from '../types'
|
||||
import { tasksApi } from '../api/client'
|
||||
|
||||
export const useTasksStore = defineStore('tasks', () => {
|
||||
// State
|
||||
const tasks = ref<Task[]>([])
|
||||
const allTasks = ref<Task[]>([])
|
||||
const currentProjectId = ref<string | null>(null)
|
||||
const selectedTask = ref<TaskWithContent | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Getters
|
||||
const activeTasks = computed(() =>
|
||||
tasks.value.filter(t => !t.completed && t.section !== 'Backlog')
|
||||
)
|
||||
|
||||
const completedTasks = computed(() =>
|
||||
tasks.value.filter(t => t.completed || t.section === 'Completed')
|
||||
)
|
||||
|
||||
const backlogTasks = computed(() =>
|
||||
tasks.value.filter(t => !t.completed && t.section === 'Backlog')
|
||||
)
|
||||
|
||||
const pendingTasks = computed(() =>
|
||||
allTasks.value.filter(t => !t.completed)
|
||||
)
|
||||
|
||||
const tasksByProject = computed(() => (projectId: string) =>
|
||||
allTasks.value.filter(t => t.project_id === projectId)
|
||||
)
|
||||
|
||||
/** All unique tags used across tasks in the current project */
|
||||
const projectTags = computed(() => {
|
||||
const tagSet = new Set<string>()
|
||||
for (const task of tasks.value) {
|
||||
if (task.tags) {
|
||||
for (const tag of task.tags) {
|
||||
tagSet.add(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...tagSet].sort()
|
||||
})
|
||||
|
||||
// Actions
|
||||
async function loadAllTasks() {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
allTasks.value = await tasksApi.listAll()
|
||||
} catch (err) {
|
||||
error.value = `Failed to load tasks: ${err}`
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProjectTasks(projectId: string) {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
currentProjectId.value = projectId
|
||||
tasks.value = await tasksApi.list(projectId)
|
||||
} catch (err) {
|
||||
error.value = `Failed to load project tasks: ${err}`
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function loadTask(projectId: string, taskId: string) {
|
||||
try {
|
||||
error.value = null
|
||||
selectedTask.value = await tasksApi.get(projectId, taskId)
|
||||
} catch (err) {
|
||||
error.value = `Failed to load task: ${err}`
|
||||
selectedTask.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function createTask(projectId: string, title: string, section?: string, parentId?: string) {
|
||||
try {
|
||||
error.value = null
|
||||
const task = await tasksApi.create(projectId, title, section, parentId)
|
||||
|
||||
// Refresh tasks list
|
||||
if (currentProjectId.value === projectId) {
|
||||
await loadProjectTasks(projectId)
|
||||
}
|
||||
|
||||
return task
|
||||
} catch (err) {
|
||||
error.value = `Failed to create task: ${err}`
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTaskContent(projectId: string, taskId: string, content: string) {
|
||||
try {
|
||||
error.value = null
|
||||
const task = await tasksApi.updateContent(projectId, taskId, content)
|
||||
selectedTask.value = task
|
||||
|
||||
// Refresh tasks list to update timestamps
|
||||
if (currentProjectId.value === projectId) {
|
||||
await loadProjectTasks(projectId)
|
||||
}
|
||||
|
||||
return task
|
||||
} catch (err) {
|
||||
error.value = `Failed to update task: ${err}`
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleTask(projectId: string, taskId: string) {
|
||||
try {
|
||||
error.value = null
|
||||
await tasksApi.toggle(projectId, taskId)
|
||||
|
||||
// Refresh tasks
|
||||
if (currentProjectId.value === projectId) {
|
||||
await loadProjectTasks(projectId)
|
||||
}
|
||||
|
||||
// Update selected task if it's the one being toggled
|
||||
if (selectedTask.value?.id === taskId) {
|
||||
await loadTask(projectId, taskId)
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = `Failed to toggle task: ${err}`
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTaskMeta(
|
||||
projectId: string,
|
||||
taskId: string,
|
||||
meta: { title?: string; section?: string; priority?: string; due_date?: string; is_active?: boolean; tags?: string[]; recurrence?: string; recurrence_interval?: number }
|
||||
) {
|
||||
try {
|
||||
error.value = null
|
||||
await tasksApi.updateMeta(projectId, taskId, meta)
|
||||
|
||||
// Refresh tasks
|
||||
if (currentProjectId.value === projectId) {
|
||||
await loadProjectTasks(projectId)
|
||||
}
|
||||
|
||||
// Update selected task if it's the one being updated
|
||||
if (selectedTask.value?.id === taskId) {
|
||||
await loadTask(projectId, taskId)
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = `Failed to update task: ${err}`
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteTask(projectId: string, taskId: string) {
|
||||
try {
|
||||
error.value = null
|
||||
await tasksApi.delete(projectId, taskId)
|
||||
|
||||
// Clear selected task if it was deleted
|
||||
if (selectedTask.value?.id === taskId) {
|
||||
selectedTask.value = null
|
||||
}
|
||||
|
||||
// Refresh tasks
|
||||
if (currentProjectId.value === projectId) {
|
||||
await loadProjectTasks(projectId)
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = `Failed to delete task: ${err}`
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
function selectTask(task: Task | null) {
|
||||
if (task && currentProjectId.value) {
|
||||
loadTask(currentProjectId.value, task.id)
|
||||
} else {
|
||||
selectedTask.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function clearSelectedTask() {
|
||||
selectedTask.value = null
|
||||
}
|
||||
|
||||
function clearProjectTasks() {
|
||||
tasks.value = []
|
||||
currentProjectId.value = null
|
||||
selectedTask.value = null
|
||||
}
|
||||
|
||||
function clearError() {
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
tasks,
|
||||
allTasks,
|
||||
currentProjectId,
|
||||
selectedTask,
|
||||
loading,
|
||||
error,
|
||||
// Getters
|
||||
activeTasks,
|
||||
completedTasks,
|
||||
backlogTasks,
|
||||
pendingTasks,
|
||||
tasksByProject,
|
||||
projectTags,
|
||||
// Actions
|
||||
loadAllTasks,
|
||||
loadProjectTasks,
|
||||
loadTask,
|
||||
createTask,
|
||||
updateTaskContent,
|
||||
toggleTask,
|
||||
updateTaskMeta,
|
||||
deleteTask,
|
||||
selectTask,
|
||||
clearSelectedTask,
|
||||
clearProjectTasks,
|
||||
clearError
|
||||
}
|
||||
})
|
||||
76
frontend/src/stores/theme.ts
Normal file
76
frontend/src/stores/theme.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export type ThemeMode = 'dark' | 'light' | 'system'
|
||||
|
||||
const STORAGE_KEY = 'ironpad-theme'
|
||||
|
||||
export const useThemeStore = defineStore('theme', () => {
|
||||
// Default to dark mode
|
||||
const mode = ref<ThemeMode>('dark')
|
||||
|
||||
// Load saved preference
|
||||
function loadSavedTheme() {
|
||||
const saved = localStorage.getItem(STORAGE_KEY)
|
||||
if (saved && ['dark', 'light', 'system'].includes(saved)) {
|
||||
mode.value = saved as ThemeMode
|
||||
}
|
||||
}
|
||||
|
||||
// Get the effective theme (resolves 'system' to actual theme)
|
||||
function getEffectiveTheme(): 'dark' | 'light' {
|
||||
if (mode.value === 'system') {
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
return mode.value
|
||||
}
|
||||
|
||||
// Apply theme to document
|
||||
function applyTheme() {
|
||||
const effectiveTheme = getEffectiveTheme()
|
||||
document.documentElement.setAttribute('data-theme', effectiveTheme)
|
||||
|
||||
// Also set class for easier CSS targeting
|
||||
document.documentElement.classList.remove('theme-dark', 'theme-light')
|
||||
document.documentElement.classList.add(`theme-${effectiveTheme}`)
|
||||
}
|
||||
|
||||
// Set theme mode
|
||||
function setTheme(newMode: ThemeMode) {
|
||||
mode.value = newMode
|
||||
localStorage.setItem(STORAGE_KEY, newMode)
|
||||
applyTheme()
|
||||
}
|
||||
|
||||
// Toggle between dark and light
|
||||
function toggleTheme() {
|
||||
const current = getEffectiveTheme()
|
||||
setTheme(current === 'dark' ? 'light' : 'dark')
|
||||
}
|
||||
|
||||
// Initialize
|
||||
function init() {
|
||||
loadSavedTheme()
|
||||
|
||||
// Ensure data-theme attribute is set (even if same as default)
|
||||
applyTheme()
|
||||
|
||||
// Listen for system theme changes when in system mode
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||
if (mode.value === 'system') {
|
||||
applyTheme()
|
||||
}
|
||||
})
|
||||
|
||||
// Theme is now initialized
|
||||
}
|
||||
|
||||
return {
|
||||
mode,
|
||||
getEffectiveTheme,
|
||||
setTheme,
|
||||
toggleTheme,
|
||||
applyTheme,
|
||||
init
|
||||
}
|
||||
})
|
||||
125
frontend/src/stores/ui.ts
Normal file
125
frontend/src/stores/ui.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { SearchResult } from '../types'
|
||||
import { searchApi } from '../api/client'
|
||||
|
||||
export const useUiStore = defineStore('ui', () => {
|
||||
// State
|
||||
const showSearch = ref(false)
|
||||
const showTasks = ref(false)
|
||||
const showPreview = ref(false)
|
||||
const searchQuery = ref('')
|
||||
const searchResults = ref<SearchResult[]>([])
|
||||
const isSearching = ref(false)
|
||||
const globalError = ref<string | null>(null)
|
||||
const sidebarSection = ref<'notes' | 'projects' | 'daily'>('notes')
|
||||
|
||||
// Getters
|
||||
const hasSearchResults = computed(() => searchResults.value.length > 0)
|
||||
|
||||
// Actions
|
||||
function openSearch() {
|
||||
showSearch.value = true
|
||||
showTasks.value = false
|
||||
}
|
||||
|
||||
function closeSearch() {
|
||||
showSearch.value = false
|
||||
searchQuery.value = ''
|
||||
searchResults.value = []
|
||||
}
|
||||
|
||||
function toggleSearch() {
|
||||
if (showSearch.value) {
|
||||
closeSearch()
|
||||
} else {
|
||||
openSearch()
|
||||
}
|
||||
}
|
||||
|
||||
function openTasks() {
|
||||
showTasks.value = true
|
||||
showSearch.value = false
|
||||
}
|
||||
|
||||
function closeTasks() {
|
||||
showTasks.value = false
|
||||
}
|
||||
|
||||
function toggleTasks() {
|
||||
if (showTasks.value) {
|
||||
closeTasks()
|
||||
} else {
|
||||
openTasks()
|
||||
}
|
||||
}
|
||||
|
||||
function togglePreview() {
|
||||
showPreview.value = !showPreview.value
|
||||
// Persist preference
|
||||
localStorage.setItem('ironpad-show-preview', String(showPreview.value))
|
||||
}
|
||||
|
||||
function loadPreviewPreference() {
|
||||
const saved = localStorage.getItem('ironpad-show-preview')
|
||||
if (saved !== null) {
|
||||
showPreview.value = saved === 'true'
|
||||
}
|
||||
}
|
||||
|
||||
async function search(query: string) {
|
||||
if (query.length < 2) {
|
||||
searchResults.value = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isSearching.value = true
|
||||
searchQuery.value = query
|
||||
searchResults.value = await searchApi.search(query)
|
||||
} catch (err) {
|
||||
globalError.value = `Search failed: ${err}`
|
||||
} finally {
|
||||
isSearching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function setSidebarSection(section: 'notes' | 'projects' | 'daily') {
|
||||
sidebarSection.value = section
|
||||
}
|
||||
|
||||
function setGlobalError(message: string | null) {
|
||||
globalError.value = message
|
||||
}
|
||||
|
||||
function clearGlobalError() {
|
||||
globalError.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
showSearch,
|
||||
showTasks,
|
||||
showPreview,
|
||||
searchQuery,
|
||||
searchResults,
|
||||
isSearching,
|
||||
globalError,
|
||||
sidebarSection,
|
||||
// Getters
|
||||
hasSearchResults,
|
||||
// Actions
|
||||
openSearch,
|
||||
closeSearch,
|
||||
toggleSearch,
|
||||
openTasks,
|
||||
closeTasks,
|
||||
toggleTasks,
|
||||
togglePreview,
|
||||
loadPreviewPreference,
|
||||
search,
|
||||
setSidebarSection,
|
||||
setGlobalError,
|
||||
clearGlobalError
|
||||
}
|
||||
})
|
||||
67
frontend/src/stores/websocket.ts
Normal file
67
frontend/src/stores/websocket.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import type { FileLock } from '../types'
|
||||
|
||||
export const useWebSocketStore = defineStore('websocket', () => {
|
||||
// State
|
||||
const connected = ref(false)
|
||||
const clientId = ref<string | null>(null)
|
||||
const fileLocks = ref<Map<string, FileLock>>(new Map())
|
||||
const gitConflicts = ref<string[]>([])
|
||||
|
||||
// Actions
|
||||
function setConnected(value: boolean) {
|
||||
connected.value = value
|
||||
}
|
||||
|
||||
function setClientId(id: string | null) {
|
||||
clientId.value = id
|
||||
}
|
||||
|
||||
function addFileLock(lock: FileLock) {
|
||||
fileLocks.value.set(lock.path, lock)
|
||||
}
|
||||
|
||||
function removeFileLock(path: string) {
|
||||
fileLocks.value.delete(path)
|
||||
}
|
||||
|
||||
function isFileLocked(path: string): FileLock | undefined {
|
||||
return fileLocks.value.get(path)
|
||||
}
|
||||
|
||||
function isFileLockedByOther(path: string): boolean {
|
||||
const lock = fileLocks.value.get(path)
|
||||
return lock !== undefined && lock.client_id !== clientId.value
|
||||
}
|
||||
|
||||
function setGitConflicts(files: string[]) {
|
||||
gitConflicts.value = files
|
||||
}
|
||||
|
||||
function clearGitConflicts() {
|
||||
gitConflicts.value = []
|
||||
}
|
||||
|
||||
function clearAllLocks() {
|
||||
fileLocks.value.clear()
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
connected,
|
||||
clientId,
|
||||
fileLocks,
|
||||
gitConflicts,
|
||||
// Actions
|
||||
setConnected,
|
||||
setClientId,
|
||||
addFileLock,
|
||||
removeFileLock,
|
||||
isFileLocked,
|
||||
isFileLockedByOther,
|
||||
setGitConflicts,
|
||||
clearGitConflicts,
|
||||
clearAllLocks
|
||||
}
|
||||
})
|
||||
76
frontend/src/stores/workspace.ts
Normal file
76
frontend/src/stores/workspace.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Project } from '../types'
|
||||
import { projectsApi } from '../api/client'
|
||||
|
||||
const STORAGE_KEY = 'ironpad-active-project'
|
||||
|
||||
export const useWorkspaceStore = defineStore('workspace', () => {
|
||||
// State
|
||||
const activeProjectId = ref<string | null>(null)
|
||||
const activeProject = ref<Project | null>(null)
|
||||
const loading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// Getters
|
||||
const hasActiveProject = computed(() => activeProjectId.value !== null)
|
||||
|
||||
// Actions
|
||||
async function setActiveProject(projectId: string | null) {
|
||||
if (projectId === activeProjectId.value) return
|
||||
|
||||
activeProjectId.value = projectId
|
||||
|
||||
if (projectId) {
|
||||
// Persist to localStorage
|
||||
localStorage.setItem(STORAGE_KEY, projectId)
|
||||
|
||||
// Load project details
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
activeProject.value = await projectsApi.get(projectId)
|
||||
} catch (err) {
|
||||
error.value = `Failed to load project: ${err}`
|
||||
activeProject.value = null
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} else {
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
activeProject.value = null
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSavedProject() {
|
||||
const savedId = localStorage.getItem(STORAGE_KEY)
|
||||
if (savedId) {
|
||||
await setActiveProject(savedId)
|
||||
}
|
||||
}
|
||||
|
||||
function clearActiveProject() {
|
||||
activeProjectId.value = null
|
||||
activeProject.value = null
|
||||
localStorage.removeItem(STORAGE_KEY)
|
||||
}
|
||||
|
||||
function clearError() {
|
||||
error.value = null
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
activeProjectId,
|
||||
activeProject,
|
||||
loading,
|
||||
error,
|
||||
// Getters
|
||||
hasActiveProject,
|
||||
// Actions
|
||||
setActiveProject,
|
||||
loadSavedProject,
|
||||
clearActiveProject,
|
||||
clearError
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user