- Normalize page titles to text-2xl font-semibold (was font-bold in TopicsView, DocumentView, SharedView) - Normalize section titles to text-lg font-semibold (was text-xl in SettingsPreferencesTab, SettingsAiTab, SettingsCloudTab, BackupCodesDisplay, CloudCredentialModal) - Normalize panel headings to text-sm font-semibold (SettingsAccountTab, DocumentView) - Normalize AdminOverviewView page title from text-xl to text-2xl font-semibold - Replace decorative sidebar skeleton inline styles with static Tailwind width classes (w-12/w-16/w-20) - Add focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 to all interactive buttons across the app - Add active:bg-* states to primary and secondary buttons for coherent press feedback - Update nav-link scoped CSS in AppSidebar and AdminSidebar to include active:bg-gray-200 and focus-visible ring - Add 7 new tests: VISUAL-01 skeleton class invariant, VISUAL-04 focus-visible invariant, VISUAL-03 typography invariant (36 test files, 270 tests pass)
125 lines
4.6 KiB
Vue
125 lines
4.6 KiB
Vue
<template>
|
|
<div>
|
|
<!-- Add form -->
|
|
<form @submit.prevent="submit" class="flex gap-2 mb-6">
|
|
<input
|
|
v-model="form.name"
|
|
type="text"
|
|
placeholder="New topic name…"
|
|
class="flex-1 border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
|
|
required
|
|
/>
|
|
<input
|
|
v-model="form.color"
|
|
type="color"
|
|
class="w-10 h-10 rounded-lg border border-gray-300 cursor-pointer p-0.5"
|
|
title="Pick color"
|
|
/>
|
|
<button
|
|
type="submit"
|
|
class="px-4 py-2 bg-indigo-600 text-white rounded-lg text-sm font-medium hover:bg-indigo-700 active:bg-indigo-800 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
|
:disabled="saving"
|
|
>
|
|
{{ saving ? 'Adding…' : 'Add' }}
|
|
</button>
|
|
</form>
|
|
|
|
<!-- Error -->
|
|
<p v-if="error" class="text-red-500 text-sm mb-4">{{ error }}</p>
|
|
|
|
<!-- Topic list -->
|
|
<div class="space-y-2">
|
|
<div
|
|
v-for="topic in topicsStore.topics"
|
|
:key="topic.id"
|
|
class="flex items-center gap-3 bg-white border border-gray-200 rounded-lg px-4 py-3"
|
|
>
|
|
<span
|
|
class="w-3 h-3 rounded-full shrink-0"
|
|
:style="{ backgroundColor: topic.color }"
|
|
></span>
|
|
|
|
<div v-if="editing === topic.id" class="flex-1 flex gap-2">
|
|
<input
|
|
v-model="editForm.name"
|
|
class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
|
|
/>
|
|
<input v-model="editForm.color" type="color" class="w-8 h-8 rounded border border-gray-300 p-0.5" />
|
|
<input
|
|
v-model="editForm.description"
|
|
placeholder="Description"
|
|
class="flex-1 border border-gray-300 rounded px-2 py-1 text-sm focus:outline-none focus:ring-2 focus:ring-indigo-400"
|
|
/>
|
|
<button @click="saveEdit(topic.id)" class="text-xs text-indigo-600 font-medium hover:text-indigo-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 rounded">Save</button>
|
|
<button @click="editing = null" class="text-xs text-gray-400 hover:text-gray-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 rounded">Cancel</button>
|
|
</div>
|
|
|
|
<div v-else class="flex-1 min-w-0">
|
|
<div class="flex items-center gap-2">
|
|
<span class="font-medium text-gray-800 text-sm">{{ topic.name }}</span>
|
|
<span class="text-xs text-gray-400">({{ topic.doc_count }} docs)</span>
|
|
</div>
|
|
<p v-if="topic.description" class="text-xs text-gray-500 mt-0.5">{{ topic.description }}</p>
|
|
</div>
|
|
|
|
<div class="flex gap-2 shrink-0">
|
|
<button @click="startEdit(topic)" class="text-xs text-gray-500 hover:text-indigo-600 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 rounded">Edit</button>
|
|
<button @click="remove(topic)" class="text-xs text-gray-500 hover:text-red-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-1 rounded">Delete</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="!topicsStore.topics.length" class="text-center py-8 text-gray-400 text-sm">
|
|
No topics yet. Add one above.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive } from 'vue'
|
|
import { useTopicsStore } from '../../stores/topics.js'
|
|
|
|
const topicsStore = useTopicsStore()
|
|
const saving = ref(false)
|
|
const error = ref(null)
|
|
const editing = ref(null)
|
|
|
|
const form = reactive({ name: '', color: '#6366f1' })
|
|
const editForm = reactive({ name: '', description: '', color: '' })
|
|
|
|
async function submit() {
|
|
saving.value = true
|
|
error.value = null
|
|
try {
|
|
await topicsStore.addTopic({ name: form.name, color: form.color })
|
|
form.name = ''
|
|
form.color = '#6366f1'
|
|
} catch (e) {
|
|
error.value = e.message
|
|
} finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
|
|
function startEdit(topic) {
|
|
editing.value = topic.id
|
|
editForm.name = topic.name
|
|
editForm.description = topic.description || ''
|
|
editForm.color = topic.color
|
|
}
|
|
|
|
async function saveEdit(id) {
|
|
await topicsStore.editTopic(id, {
|
|
name: editForm.name,
|
|
description: editForm.description,
|
|
color: editForm.color,
|
|
})
|
|
editing.value = null
|
|
}
|
|
|
|
async function remove(topic) {
|
|
if (!confirm(`Delete topic "${topic.name}"? It will be removed from all documents.`)) return
|
|
await topicsStore.removeTopic(topic.id)
|
|
}
|
|
</script>
|