feat(11-05): visual consistency pass — typography, focus-visible, hover/active states, skeleton cleanup
- 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)
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="p-8 max-w-4xl mx-auto">
|
||||
<!-- Back -->
|
||||
<button @click="$router.back()" class="text-sm text-indigo-600 hover:underline mb-6 flex items-center gap-1">
|
||||
<button @click="$router.back()" class="text-sm text-indigo-600 hover:underline mb-6 flex items-center gap-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 rounded">
|
||||
← Back
|
||||
</button>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<!-- Header -->
|
||||
<div class="flex items-start justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-900 break-all">{{ doc.original_name }}</h2>
|
||||
<h2 class="text-2xl font-semibold text-gray-900 break-all">{{ doc.original_name }}</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">
|
||||
Uploaded {{ formatDate(doc.created_at) }} · {{ formatSize(doc.size_bytes) }} · {{ doc.mime_type }}
|
||||
</p>
|
||||
@@ -22,13 +22,13 @@
|
||||
<button
|
||||
v-if="isPdf"
|
||||
@click="openPdf"
|
||||
class="text-sm px-3 py-1.5 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors"
|
||||
class="text-sm px-3 py-1.5 bg-indigo-600 text-white rounded-lg 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"
|
||||
>
|
||||
{{ pdfOpenMode === 'in_app' ? 'Preview' : 'Open' }}
|
||||
</button>
|
||||
<button
|
||||
@click="confirmDelete"
|
||||
class="text-sm text-red-500 hover:text-red-700"
|
||||
class="text-sm text-red-500 hover:text-red-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-1 rounded"
|
||||
>Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -36,19 +36,19 @@
|
||||
<!-- Topics -->
|
||||
<div class="bg-white border border-gray-200 rounded-xl p-5 mb-5">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="font-semibold text-gray-800">Topics</h3>
|
||||
<h3 class="text-sm font-semibold text-gray-800">Topics</h3>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="reclassify"
|
||||
:disabled="classifying"
|
||||
class="text-xs px-3 py-1.5 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition-colors disabled:opacity-50"
|
||||
class="text-xs px-3 py-1.5 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 active:bg-indigo-800 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
{{ classifying ? 'Classifying…' : 'Re-classify' }}
|
||||
</button>
|
||||
<button
|
||||
@click="suggestTopics"
|
||||
:disabled="suggesting"
|
||||
class="text-xs px-3 py-1.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
|
||||
class="text-xs px-3 py-1.5 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 active:bg-gray-100 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
{{ suggesting ? 'Suggesting…' : 'Suggest Topics' }}
|
||||
</button>
|
||||
@@ -97,7 +97,7 @@
|
||||
|
||||
<!-- Extracted text -->
|
||||
<div class="bg-white border border-gray-200 rounded-xl p-5">
|
||||
<h3 class="font-semibold text-gray-800 mb-3">Extracted Text</h3>
|
||||
<h3 class="text-sm font-semibold text-gray-800 mb-3">Extracted Text</h3>
|
||||
<pre class="text-xs text-gray-600 whitespace-pre-wrap font-mono bg-gray-50 rounded-lg p-4 max-h-96 overflow-y-auto">{{ doc.extracted_text || '(no text extracted)' }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
@@ -132,11 +132,11 @@
|
||||
<div class="flex gap-2 justify-end">
|
||||
<button
|
||||
@click="cancelCloudDeleteWarning"
|
||||
class="border border-gray-300 text-gray-700 text-sm px-4 py-2 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
class="border border-gray-300 text-gray-700 text-sm px-4 py-2 rounded-lg hover:bg-gray-50 active:bg-gray-100 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>Cancel</button>
|
||||
<button
|
||||
@click="confirmRemoveOnly"
|
||||
class="bg-red-600 hover:bg-red-700 text-white text-sm px-4 py-2 rounded-lg transition-colors"
|
||||
class="bg-red-600 hover:bg-red-700 active:bg-red-800 text-white text-sm px-4 py-2 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-1"
|
||||
>Remove from app</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<button
|
||||
@click="oauthSuccessProvider = null"
|
||||
aria-label="Dismiss notification"
|
||||
class="text-gray-400 hover:text-gray-600 shrink-0"
|
||||
class="text-gray-400 hover:text-gray-600 shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 rounded"
|
||||
>
|
||||
<AppIcon name="x" class="w-4 h-4" />
|
||||
</button>
|
||||
@@ -30,7 +30,7 @@
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
@click="activeTab = tab.id"
|
||||
class="px-4 py-2 text-sm font-semibold border-b-2 transition-colors"
|
||||
class="px-4 py-2 text-sm font-semibold border-b-2 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 rounded-t"
|
||||
:class="activeTab === tab.id
|
||||
? 'text-indigo-600 border-indigo-600'
|
||||
: 'text-gray-500 hover:text-gray-700 border-transparent'"
|
||||
@@ -64,7 +64,7 @@
|
||||
<button
|
||||
@click="oauthError = null"
|
||||
aria-label="Dismiss error"
|
||||
class="text-red-400 hover:text-red-600 shrink-0"
|
||||
class="text-red-400 hover:text-red-600 shrink-0 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-red-500 focus-visible:ring-offset-1 rounded"
|
||||
>
|
||||
<AppIcon name="x" class="w-4 h-4" />
|
||||
</button>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="p-8 max-w-4xl mx-auto">
|
||||
<BreadcrumbBar :segments="[{ label: 'Shared with me' }]" :show-root="false" class="mb-4" />
|
||||
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-1">Shared with me</h2>
|
||||
<h2 class="text-2xl font-semibold text-gray-900 mb-1">Shared with me</h2>
|
||||
<p class="text-gray-500 text-sm mb-6">Documents other users have shared with you.</p>
|
||||
|
||||
<div v-if="loading" class="text-sm text-gray-400">Loading…</div>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-gray-900">
|
||||
<h2 class="text-2xl font-semibold text-gray-900">
|
||||
{{ activeTopic ? activeTopic : 'All Topics' }}
|
||||
</h2>
|
||||
<p class="text-gray-500 text-sm mt-0.5">
|
||||
@@ -13,7 +13,7 @@
|
||||
<button
|
||||
v-if="activeTopic"
|
||||
@click="$router.push('/topics')"
|
||||
class="text-sm text-indigo-600 hover:underline"
|
||||
class="text-sm text-indigo-600 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 rounded"
|
||||
>
|
||||
← All Topics
|
||||
</button>
|
||||
@@ -31,7 +31,7 @@
|
||||
v-for="topic in topicsStore.topics"
|
||||
:key="topic.id"
|
||||
:to="`/topics/${encodeURIComponent(topic.name)}`"
|
||||
class="bg-white border border-gray-200 rounded-xl p-4 hover:border-indigo-300 hover:shadow-sm transition-all"
|
||||
class="bg-white border border-gray-200 rounded-xl p-4 hover:border-indigo-300 hover:shadow-sm transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="w-3 h-3 rounded-full" :style="{ backgroundColor: topic.color }"></span>
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
/**
|
||||
* VISUAL-01 / VISUAL-03: Typography normalization invariants
|
||||
*
|
||||
* Tests that page-level h2 headings use text-2xl font-semibold (not font-bold),
|
||||
* and section-level h3 headings inside cards use text-sm or text-lg font-semibold (not text-xl).
|
||||
*/
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
||||
import { mount, flushPromises } from '@vue/test-utils'
|
||||
import { setActivePinia, createPinia } from 'pinia'
|
||||
import { createRouter, createMemoryHistory } from 'vue-router'
|
||||
|
||||
vi.mock('../../api/client.js', () => ({
|
||||
getSharedWithMe: vi.fn().mockResolvedValue([]),
|
||||
listFolders: vi.fn().mockResolvedValue({ items: [] }),
|
||||
getDocument: vi.fn().mockResolvedValue(null),
|
||||
getMyPreferences: vi.fn().mockResolvedValue({ pdf_open_mode: 'new_tab' }),
|
||||
listTopics: vi.fn().mockResolvedValue({ topics: [] }),
|
||||
getMyQuota: vi.fn().mockResolvedValue({ used_bytes: 0, limit_bytes: 104857600 }),
|
||||
}))
|
||||
|
||||
function makeRouter(extraRoutes = []) {
|
||||
return createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes: [
|
||||
{ path: '/', component: { template: '<div/>' } },
|
||||
{ path: '/topics', component: { template: '<div/>' } },
|
||||
{ path: '/shared', component: { template: '<div/>' } },
|
||||
{ path: '/settings', component: { template: '<div/>' } },
|
||||
...extraRoutes,
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
describe('VISUAL-03: SharedView page title uses font-semibold (not font-bold)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
it('h2 has text-2xl font-semibold and not font-bold', async () => {
|
||||
vi.doMock('../../api/client.js', () => ({
|
||||
getSharedWithMe: vi.fn().mockResolvedValue([]),
|
||||
listFolders: vi.fn().mockResolvedValue({ items: [] }),
|
||||
getDocument: vi.fn().mockResolvedValue(null),
|
||||
getMyPreferences: vi.fn().mockResolvedValue({ pdf_open_mode: 'new_tab' }),
|
||||
listTopics: vi.fn().mockResolvedValue({ topics: [] }),
|
||||
getMyQuota: vi.fn().mockResolvedValue({ used_bytes: 0, limit_bytes: 104857600 }),
|
||||
}))
|
||||
|
||||
setActivePinia(createPinia())
|
||||
const router = makeRouter()
|
||||
await router.push('/shared')
|
||||
await router.isReady()
|
||||
|
||||
const SharedView = (await import('../SharedView.vue')).default
|
||||
const wrapper = mount(SharedView, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: { BreadcrumbBar: true, EmptyState: true, AppIcon: true },
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
const h2 = wrapper.find('h2')
|
||||
expect(h2.exists()).toBe(true)
|
||||
const cls = h2.attributes('class') || ''
|
||||
expect(cls).toContain('font-semibold')
|
||||
expect(cls).not.toContain('font-bold')
|
||||
expect(cls).toContain('text-2xl')
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('VISUAL-03: TopicsView page title uses font-semibold (not font-bold)', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
it('h2 has text-2xl font-semibold and not font-bold', async () => {
|
||||
vi.doMock('../../api/client.js', () => ({
|
||||
getSharedWithMe: vi.fn().mockResolvedValue([]),
|
||||
listFolders: vi.fn().mockResolvedValue({ items: [] }),
|
||||
getDocument: vi.fn().mockResolvedValue(null),
|
||||
getMyPreferences: vi.fn().mockResolvedValue({ pdf_open_mode: 'new_tab' }),
|
||||
listTopics: vi.fn().mockResolvedValue({ topics: [] }),
|
||||
getMyQuota: vi.fn().mockResolvedValue({ used_bytes: 0, limit_bytes: 104857600 }),
|
||||
}))
|
||||
|
||||
setActivePinia(createPinia())
|
||||
const router = makeRouter([
|
||||
{ path: '/topics/:name?', component: { template: '<div/>' } },
|
||||
])
|
||||
await router.push('/topics')
|
||||
await router.isReady()
|
||||
|
||||
const TopicsView = (await import('../TopicsView.vue')).default
|
||||
const wrapper = mount(TopicsView, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: { TopicManager: true, DocumentCard: true, BreadcrumbBar: true },
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
const h2 = wrapper.find('h2')
|
||||
expect(h2.exists()).toBe(true)
|
||||
const cls = h2.attributes('class') || ''
|
||||
expect(cls).toContain('font-semibold')
|
||||
expect(cls).not.toContain('font-bold')
|
||||
expect(cls).toContain('text-2xl')
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
|
||||
describe('VISUAL-03: SettingsAccountTab section headings use text-sm font-semibold', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
it('h3 inside bg-white panel does not use text-xl or font-bold', async () => {
|
||||
vi.doMock('../../api/client.js', () => ({
|
||||
getSharedWithMe: vi.fn().mockResolvedValue([]),
|
||||
listFolders: vi.fn().mockResolvedValue({ items: [] }),
|
||||
getDocument: vi.fn().mockResolvedValue(null),
|
||||
getMyPreferences: vi.fn().mockResolvedValue({ pdf_open_mode: 'new_tab' }),
|
||||
listTopics: vi.fn().mockResolvedValue({ topics: [] }),
|
||||
getMyQuota: vi.fn().mockResolvedValue({ used_bytes: 0, limit_bytes: 104857600 }),
|
||||
}))
|
||||
|
||||
setActivePinia(createPinia())
|
||||
|
||||
const { useAuthStore } = await import('../../stores/auth.js')
|
||||
const authStore = useAuthStore()
|
||||
authStore.user = { email: 'test@example.com', role: 'user', totp_enabled: false, handle: 'test' }
|
||||
|
||||
const router = makeRouter()
|
||||
await router.push('/settings')
|
||||
await router.isReady()
|
||||
|
||||
const SettingsAccountTab = (await import('../../components/settings/SettingsAccountTab.vue')).default
|
||||
const wrapper = mount(SettingsAccountTab, {
|
||||
global: {
|
||||
plugins: [router],
|
||||
stubs: {
|
||||
TotpEnrollment: true,
|
||||
ConfirmBlock: true,
|
||||
PasswordStrengthBar: true,
|
||||
AppIcon: true,
|
||||
AppSpinner: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
await flushPromises()
|
||||
|
||||
const headings = wrapper.findAll('h3')
|
||||
expect(headings.length).toBeGreaterThan(0)
|
||||
|
||||
for (const h of headings) {
|
||||
const cls = h.attributes('class') || ''
|
||||
expect(cls).not.toContain('text-xl')
|
||||
expect(cls).not.toContain('font-bold')
|
||||
expect(cls).toContain('font-semibold')
|
||||
}
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
})
|
||||
@@ -28,7 +28,7 @@
|
||||
:class="['transition-colors', prov.is_active ? 'border-l-4 border-l-green-500' : '']"
|
||||
>
|
||||
<button
|
||||
class="w-full flex items-center justify-between px-6 py-3 text-left hover:bg-gray-50 transition-colors"
|
||||
class="w-full flex items-center justify-between px-6 py-3 text-left hover:bg-gray-50 active:bg-gray-100 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-indigo-500"
|
||||
@click="toggleProvider(prov.provider_id)"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -91,7 +91,7 @@
|
||||
<button
|
||||
@click="saveSystemProvider(prov.provider_id, { activate: true })"
|
||||
:disabled="savingProvider === prov.provider_id || prov.is_active"
|
||||
class="text-xs bg-green-600 text-white font-semibold px-3 py-1.5 rounded-lg hover:bg-green-700 disabled:opacity-50 transition-colors"
|
||||
class="text-xs bg-green-600 text-white font-semibold px-3 py-1.5 rounded-lg hover:bg-green-700 active:bg-green-800 disabled:opacity-50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-green-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
Set Active
|
||||
</button>
|
||||
@@ -99,7 +99,7 @@
|
||||
<button
|
||||
@click="saveSystemProvider(prov.provider_id)"
|
||||
:disabled="savingProvider === prov.provider_id"
|
||||
class="text-xs bg-indigo-600 text-white font-semibold px-3 py-1.5 rounded-lg hover:bg-indigo-700 disabled:opacity-50 flex items-center gap-1 transition-colors"
|
||||
class="text-xs bg-indigo-600 text-white font-semibold px-3 py-1.5 rounded-lg hover:bg-indigo-700 active:bg-indigo-800 disabled:opacity-50 flex items-center gap-1 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
<span v-if="savingProvider === prov.provider_id" class="animate-spin rounded-full border-2 border-current border-t-transparent w-3 h-3"></span>
|
||||
{{ savingProvider === prov.provider_id ? 'Saving…' : 'Save' }}
|
||||
@@ -108,7 +108,7 @@
|
||||
<button
|
||||
@click="runTestConnection(prov.provider_id)"
|
||||
:disabled="testingProvider === prov.provider_id || savingProvider === prov.provider_id"
|
||||
class="text-xs border border-gray-300 text-gray-700 font-semibold px-3 py-1.5 rounded-lg hover:bg-gray-100 disabled:opacity-50 flex items-center gap-1.5 transition-colors"
|
||||
class="text-xs border border-gray-300 text-gray-700 font-semibold px-3 py-1.5 rounded-lg hover:bg-gray-100 active:bg-gray-200 disabled:opacity-50 flex items-center gap-1.5 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
<span
|
||||
v-if="testingProvider === prov.provider_id"
|
||||
@@ -178,7 +178,7 @@
|
||||
<button
|
||||
@click="saveConfig(user.id)"
|
||||
:disabled="savingId === user.id"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-700 font-semibold disabled:opacity-50 flex items-center gap-1"
|
||||
class="text-sm text-indigo-600 hover:text-indigo-700 font-semibold disabled:opacity-50 flex items-center gap-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1 rounded"
|
||||
>
|
||||
<span v-if="savingId === user.id" class="animate-spin rounded-full border-2 border-current border-t-transparent w-3 h-3"></span>
|
||||
{{ savingId === user.id ? 'Saving…' : 'Save' }}
|
||||
|
||||
@@ -44,14 +44,14 @@
|
||||
</div>
|
||||
<button
|
||||
@click="applyFilters"
|
||||
class="bg-indigo-600 text-white text-sm px-4 py-2 rounded-lg hover:bg-indigo-700 transition-colors"
|
||||
class="bg-indigo-600 text-white text-sm px-4 py-2 rounded-lg 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"
|
||||
>
|
||||
Apply filters
|
||||
</button>
|
||||
<button
|
||||
v-if="activeFilterCount > 0"
|
||||
@click="clearFilters"
|
||||
class="border border-gray-300 text-gray-500 text-sm px-4 py-2 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
class="border border-gray-300 text-gray-500 text-sm px-4 py-2 rounded-lg hover:bg-gray-50 active:bg-gray-100 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
@@ -59,7 +59,7 @@
|
||||
<button
|
||||
@click="exportCsv"
|
||||
:disabled="exportingCsv"
|
||||
class="border border-gray-300 text-gray-700 text-sm px-4 py-2 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
|
||||
class="border border-gray-300 text-gray-700 text-sm px-4 py-2 rounded-lg hover:bg-gray-50 active:bg-gray-100 transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
<span v-if="exportingCsv" class="flex items-center gap-1">
|
||||
<span class="animate-spin rounded-full border-2 border-current border-t-transparent w-3 h-3"></span>
|
||||
@@ -141,7 +141,7 @@
|
||||
<button
|
||||
@click="prevPage"
|
||||
:disabled="page <= 1"
|
||||
class="text-sm text-gray-600 hover:text-gray-900 disabled:opacity-40 disabled:cursor-not-allowed px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
class="text-sm text-gray-600 hover:text-gray-900 disabled:opacity-40 disabled:cursor-not-allowed px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 active:bg-gray-100 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
@@ -149,7 +149,7 @@
|
||||
<button
|
||||
@click="nextPage"
|
||||
:disabled="page * perPage >= total"
|
||||
class="text-sm text-gray-600 hover:text-gray-900 disabled:opacity-40 disabled:cursor-not-allowed px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
class="text-sm text-gray-600 hover:text-gray-900 disabled:opacity-40 disabled:cursor-not-allowed px-3 py-1.5 border border-gray-200 rounded-lg hover:bg-gray-50 active:bg-gray-100 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
@@ -183,7 +183,7 @@
|
||||
<button
|
||||
@click="downloadDailyExport"
|
||||
:disabled="!selectedExportDate || downloadingExport"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 text-white text-sm px-4 py-2 rounded-lg disabled:opacity-50 transition-colors flex items-center gap-1"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white text-sm px-4 py-2 rounded-lg disabled:opacity-50 transition-colors flex items-center gap-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
<span v-if="downloadingExport" class="animate-spin rounded-full border-2 border-current border-t-transparent w-3 h-3"></span>
|
||||
Download
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<BreadcrumbBar :segments="[]" :show-root="false" class="mb-4" />
|
||||
|
||||
<h2 class="text-xl font-semibold text-gray-900 mb-6">Overview</h2>
|
||||
<h2 class="text-2xl font-semibold text-gray-900 mb-6">Overview</h2>
|
||||
|
||||
<div v-if="loading" class="text-gray-500">Loading overview…</div>
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<button
|
||||
@click="submitCreate"
|
||||
:disabled="creating"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold px-4 py-2 rounded-lg disabled:opacity-50 transition-colors"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white text-sm font-semibold px-4 py-2 rounded-lg disabled:opacity-50 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
<span v-if="creating" class="flex items-center gap-1.5">
|
||||
<span class="animate-spin rounded-full border-2 border-current border-t-transparent w-4 h-4"></span>
|
||||
@@ -65,7 +65,7 @@
|
||||
</button>
|
||||
<button
|
||||
@click="cancelCreate"
|
||||
class="text-sm px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
class="text-sm px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 active:bg-gray-100 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
@@ -78,7 +78,7 @@
|
||||
<button
|
||||
v-if="!showCreateForm"
|
||||
@click="openCreateForm"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors"
|
||||
class="bg-indigo-600 hover:bg-indigo-700 active:bg-indigo-800 text-white text-sm font-semibold px-4 py-2 rounded-lg transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-1"
|
||||
>
|
||||
Create user
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user