Move new-project form to the dashboard; sidebar project list becomes a switcher; Kanban is the default project tab

- DashboardView: a "New project title" form now sits directly below the
  project grid (creating one stays on the dashboard -- the grid and the
  sidebar dropdown pick it up via the shared projects store, no navigation).
- AppSidebar: the per-project RouterLink list is replaced by a <select>. It's
  a v-model-bound writable computed (selectedProjectId): the getter tracks
  route.params.id so it reflects whichever project is open, the setter
  router.pushes to the chosen one -- so it doubles as a project switcher from
  anywhere, not just a picker from the dashboard. Removed the now-unused
  .sidebar__link--project/.sidebar__projects/.sidebar__form CSS; added
  .sidebar__select, and .form--new-project back for the dashboard form.
- ProjectView: activeTab now initialises to 'kanban' instead of 'all'. The
  tabs array (and so the tab bar's DOM order, All tasks first) is untouched --
  only the default selection changed.

Verified via headless Chrome: sidebar has no per-project links (a <select>
listing both projects instead); the new-project form follows the grid and
creating from it stays on /dashboard while the grid and dropdown both update;
selecting a project in the dropdown navigates there and the dropdown reflects
it once there; a project opens on Kanban with the tab bar still reading "All
tasks, Kanban" in that order.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 15:31:30 +01:00
co-authored by Claude Sonnet 5
parent 4ee0f24078
commit d8a4183e4d
6 changed files with 104 additions and 103 deletions
+45 -4
View File
@@ -1,13 +1,19 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { RouterLink } from 'vue-router'
import { ApiError } from '../lib/api'
import { useProjectsStore } from '../stores/projects'
import { MAX_PROJECTS, useProjectsStore } from '../stores/projects'
const projects = useProjectsStore()
const loadError = ref<string | null>(null)
const title = ref('')
const createError = ref<ApiError | null>(null)
const submitting = ref(false)
const atLimit = computed(() => projects.projects.length >= MAX_PROJECTS)
onMounted(load)
async function load() {
@@ -18,6 +24,19 @@ async function load() {
loadError.value = e instanceof ApiError ? e.message : 'Could not load the dashboard.'
}
}
async function onCreate() {
submitting.value = true
createError.value = null
try {
await projects.createProject(title.value)
title.value = ''
} catch (e) {
createError.value = e instanceof ApiError ? e : new ApiError('Could not create the project.', 0)
} finally {
submitting.value = false
}
}
</script>
<template>
@@ -25,8 +44,8 @@ async function load() {
<p v-if="loadError" class="form-error">{{ loadError }}</p>
<p v-else-if="projects.loading && !projects.loaded" class="muted">Loading</p>
<p v-else-if="projects.projects.length === 0" class="muted">
No projects yet create one from the sidebar. Anything uncategorised
lives in the inbox, also in the sidebar.
No projects yet create one below. Anything uncategorised lives in the
inbox, in the sidebar.
</p>
<div v-else class="dashboard__grid">
@@ -42,5 +61,27 @@ async function load() {
</span>
</RouterLink>
</div>
<form class="form form--new-project" @submit.prevent="onCreate">
<label>
<span>New project title</span>
<input v-model="title" type="text" maxlength="255" required :disabled="atLimit" />
<small v-if="createError?.fieldError('title')" class="field-error">
{{ createError.fieldError('title') }}
</small>
</label>
<p v-if="createError && Object.keys(createError.details).length === 0" class="form-error">
{{ createError.message }}
</p>
<button type="submit" :disabled="submitting || atLimit">
{{ submitting ? 'Creating…' : 'Create project' }}
</button>
<p v-if="atLimit" class="hint">
You have reached the maximum of {{ MAX_PROJECTS }} projects.
</p>
</form>
</div>
</template>
+2 -1
View File
@@ -23,11 +23,12 @@ const statuses = ref<CardStatus[]>([])
const loadError = ref<string | null>(null)
const actionError = ref<string | null>(null)
// Tab order is unchanged (All tasks first); Kanban is just the default view.
const tabs = [
{ key: 'all', label: 'All tasks' },
{ key: 'kanban', label: 'Kanban' },
] as const
const activeTab = ref<(typeof tabs)[number]['key']>('all')
const activeTab = ref<(typeof tabs)[number]['key']>('kanban')
const titleDraft = ref('')
const descriptionDraft = ref('')