feat: add persisted List/Board view toggle to Chorus

This commit is contained in:
pyr0ball 2026-07-14 13:23:23 -07:00
parent cee0dcd36a
commit 18cfd22db5
2 changed files with 110 additions and 7 deletions

View file

@ -1,6 +1,7 @@
<script setup>
import { ref, onMounted } from 'vue'
import { ref, onMounted, watch } from 'vue'
import TriageList from './components/TriageList.vue'
import KanbanBoard from './components/KanbanBoard.vue'
import ItemModal from './components/ItemModal.vue'
import { fetchItems, fetchItem } from './api.js'
@ -9,6 +10,14 @@ const selectedItem = ref(null)
const includeDone = ref(false)
const errorMessage = ref('')
const VIEW_MODE_KEY = 'chorus_view_mode'
const storedViewMode = localStorage.getItem(VIEW_MODE_KEY)
const viewMode = ref(storedViewMode === 'board' ? 'board' : 'list')
watch(viewMode, (mode) => {
localStorage.setItem(VIEW_MODE_KEY, mode)
})
async function loadItems() {
try {
errorMessage.value = ''
@ -49,10 +58,29 @@ onMounted(async () => {
<div class="container">
<h1>Chorus</h1>
<label class="include-done-toggle">
<input type="checkbox" v-model="includeDone" @change="loadItems" />
Show completed
</label>
<div class="view-controls">
<label class="include-done-toggle">
<input type="checkbox" v-model="includeDone" @change="loadItems" />
Show completed
</label>
<div class="view-toggle">
<button
type="button"
:class="{ active: viewMode === 'list' }"
@click="viewMode = 'list'"
>
List
</button>
<button
type="button"
:class="{ active: viewMode === 'board' }"
@click="viewMode = 'board'"
>
Board
</button>
</div>
</div>
<p v-if="errorMessage" class="status-message">{{ errorMessage }}</p>
@ -60,22 +88,52 @@ onMounted(async () => {
Nothing to triage right now.
</p>
<TriageList v-else :items="items" @select-item="openItem" />
<TriageList v-else-if="viewMode === 'list'" :items="items" @select-item="openItem" />
<KanbanBoard v-else :items="items" @select-item="openItem" />
<ItemModal :item="selectedItem" @close="closeModal" @saved="onSaved" />
</div>
</template>
<style scoped>
.view-controls {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.include-done-toggle {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
color: var(--color-text-muted);
font-size: 0.9rem;
}
.view-toggle {
display: flex;
gap: var(--spacing-sm);
}
.view-toggle button {
min-height: 44px;
padding: var(--spacing-sm) var(--spacing-md);
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
color: var(--color-text);
cursor: pointer;
}
.view-toggle button.active {
background: var(--color-accent);
color: white;
border-color: var(--color-accent);
}
.status-message {
color: var(--color-text-muted);
padding: var(--spacing-md);

View file

@ -0,0 +1,45 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mount, flushPromises } from '@vue/test-utils'
import App from '../src/App.vue'
vi.mock('../src/api.js', () => ({
fetchItems: vi.fn(async () => []),
fetchItem: vi.fn(async (id) => ({ id })),
}))
beforeEach(() => {
localStorage.clear()
})
describe('App view mode', () => {
it('defaults to list view when nothing is stored', async () => {
const wrapper = mount(App)
await flushPromises()
const listButton = wrapper.findAll('.view-toggle button').find((b) => b.text() === 'List')
expect(listButton.classes()).toContain('active')
})
it('honors a stored board view mode on mount', async () => {
localStorage.setItem('chorus_view_mode', 'board')
const wrapper = mount(App)
await flushPromises()
const boardButton = wrapper.findAll('.view-toggle button').find((b) => b.text() === 'Board')
expect(boardButton.classes()).toContain('active')
})
it('defaults to list view for an invalid stored value', async () => {
localStorage.setItem('chorus_view_mode', 'garbage')
const wrapper = mount(App)
await flushPromises()
const listButton = wrapper.findAll('.view-toggle button').find((b) => b.text() === 'List')
expect(listButton.classes()).toContain('active')
})
it('clicking Board writes the choice to localStorage', async () => {
const wrapper = mount(App)
await flushPromises()
const boardButton = wrapper.findAll('.view-toggle button').find((b) => b.text() === 'Board')
await boardButton.trigger('click')
expect(localStorage.getItem('chorus_view_mode')).toBe('board')
})
})