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
+22 -60
View File
@@ -6,7 +6,7 @@ import { ApiError } from '../lib/api'
import { reorderColumn } from '../lib/cardOrder'
import { useCardsStore } from '../stores/cards'
import { useInboxStore } from '../stores/inbox'
import { MAX_PROJECTS, useProjectsStore } from '../stores/projects'
import { useProjectsStore } from '../stores/projects'
import type { Card } from '../types'
import KanbanCard from './KanbanCard.vue'
@@ -16,13 +16,8 @@ const inbox = useInboxStore()
const router = useRouter()
const route = useRoute()
const title = ref('')
const createError = ref<ApiError | null>(null)
const submitting = ref(false)
const loadError = ref<string | null>(null)
const atLimit = computed(() => projects.projects.length >= MAX_PROJECTS)
onMounted(() => {
if (!projects.loaded) void loadProjects()
if (!inbox.loaded) void loadInbox()
@@ -37,19 +32,14 @@ async function loadProjects() {
}
}
async function onCreate() {
submitting.value = true
createError.value = null
try {
const project = await projects.createProject(title.value)
title.value = ''
await router.push({ name: 'project', params: { id: project.id } })
} catch (e) {
createError.value = e instanceof ApiError ? e : new ApiError('Could not create the project.', 0)
} finally {
submitting.value = false
}
}
// The dropdown doubles as a project switcher: it reflects whichever project
// (if any) is currently open, and selecting one navigates there.
const selectedProjectId = computed<number | ''>({
get: () => (route.name === 'project' ? Number(route.params.id) : ''),
set: (id) => {
if (id !== '') void router.push({ name: 'project', params: { id } })
},
})
// --- Inbox: a global holding area, not tied to any project. Cards drag in
// and out of it from a project's kanban board (shared "kanban" group).
@@ -126,47 +116,19 @@ async function onInboxChange(change: ColumnChange) {
<p v-else-if="projects.loading && !projects.loaded" class="sidebar__note muted">Loading</p>
<p v-else-if="projects.projects.length === 0" class="sidebar__note muted">No projects yet.</p>
<nav v-else class="sidebar__nav sidebar__projects">
<RouterLink
v-for="project in projects.projects"
:key="project.id"
:to="{ name: 'project', params: { id: project.id } }"
class="sidebar__link sidebar__link--project"
active-class="sidebar__link--active"
>
<svg class="sidebar__icon" viewBox="0 0 16 16" aria-hidden="true">
<rect x="1" y="2" width="3.5" height="12" rx="1" />
<rect x="6.25" y="2" width="3.5" height="9" rx="1" />
<rect x="11.5" y="2" width="3.5" height="6" rx="1" />
</svg>
<span class="sidebar__link-text">{{ project.title }}</span>
</RouterLink>
</nav>
<form class="sidebar__form" @submit.prevent="onCreate">
<input
v-model="title"
type="text"
maxlength="255"
required
:disabled="atLimit"
placeholder="New project"
aria-label="New project title"
/>
<small v-if="createError?.fieldError('title')" class="field-error">
{{ createError.fieldError('title') }}
</small>
<small
v-else-if="createError && Object.keys(createError.details).length === 0"
class="field-error"
>
{{ createError.message }}
</small>
<button type="submit" :disabled="submitting || atLimit">
{{ submitting ? 'Creating…' : 'Add project' }}
</button>
<small v-if="atLimit" class="hint">Limit of {{ MAX_PROJECTS }} projects reached.</small>
</form>
<div v-else class="sidebar__select">
<svg class="sidebar__icon" viewBox="0 0 16 16" aria-hidden="true">
<rect x="1" y="2" width="3.5" height="12" rx="1" />
<rect x="6.25" y="2" width="3.5" height="9" rx="1" />
<rect x="11.5" y="2" width="3.5" height="6" rx="1" />
</svg>
<select v-model="selectedProjectId" aria-label="Go to project">
<option value="" disabled>Choose a project</option>
<option v-for="project in projects.projects" :key="project.id" :value="project.id">
{{ project.title }}
</option>
</select>
</div>
<hr class="sidebar__divider" />