- Custom tooltip directive (WCAG AAA) on every button in the app - Two-column timer layout with sticky hero and recent entries sidebar - Timer font selector with 16 monospace Google Fonts and live preview - UI font selector with 15+ Google Fonts - Close-to-tray and minimize-to-tray settings - New app icons (no-glow variants), platform icon set - Mini timer pop-out window - Favorites strip with drag-reorder and inline actions - Comprehensive README with feature documentation - Remove tracked files that belong in gitignore
140 lines
4.1 KiB
Vue
140 lines
4.1 KiB
Vue
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import InvoicePreview from './InvoicePreview.vue'
|
|
import {
|
|
TEMPLATE_CATEGORIES,
|
|
getTemplatesByCategory,
|
|
getTemplateById,
|
|
} from '../utils/invoiceTemplates'
|
|
import type { InvoiceItem } from '../utils/invoicePdf'
|
|
import type { BusinessInfo } from '../utils/invoicePdfRenderer'
|
|
import type { Invoice } from '../stores/invoices'
|
|
import type { Client } from '../stores/clients'
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
modelValue: string
|
|
invoice?: Invoice
|
|
client?: Client | null
|
|
items?: InvoiceItem[]
|
|
businessInfo?: BusinessInfo
|
|
}>(),
|
|
{
|
|
invoice: undefined,
|
|
client: undefined,
|
|
items: undefined,
|
|
businessInfo: undefined,
|
|
},
|
|
)
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: string]
|
|
}>()
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Default sample data
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const defaultInvoice: Invoice = {
|
|
client_id: 0,
|
|
invoice_number: 'INV-2026-001',
|
|
date: '2026-02-18',
|
|
due_date: '2026-03-18',
|
|
subtotal: 8800,
|
|
tax_rate: 10,
|
|
tax_amount: 880,
|
|
discount: 0,
|
|
total: 9680,
|
|
notes: 'Payment due within 30 days. Thank you for your business!',
|
|
status: 'pending',
|
|
}
|
|
|
|
const defaultClient: Client = {
|
|
id: 1,
|
|
name: 'Acme Corporation',
|
|
email: 'billing@acme.com',
|
|
address: '123 Business Ave\nSuite 100\nNew York, NY 10001',
|
|
}
|
|
|
|
const defaultBusinessInfo: BusinessInfo = {
|
|
name: 'Your Business Name',
|
|
address: '456 Creative St, Design City',
|
|
email: 'hello@business.com',
|
|
phone: '(555) 123-4567',
|
|
logo: '',
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Computed
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const selectedTemplate = computed(() => getTemplateById(props.modelValue))
|
|
|
|
const previewInvoice = computed(() => props.invoice ?? defaultInvoice)
|
|
|
|
const previewClient = computed(() =>
|
|
props.client !== undefined ? props.client : defaultClient,
|
|
)
|
|
|
|
const previewItems = computed(() =>
|
|
props.items && props.items.length > 0 ? props.items : [],
|
|
)
|
|
|
|
const previewBusinessInfo = computed(() => props.businessInfo ?? defaultBusinessInfo)
|
|
|
|
function selectTemplate(id: string) {
|
|
emit('update:modelValue', id)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div
|
|
class="flex border border-border-subtle rounded-lg overflow-hidden"
|
|
style="height: 480px"
|
|
>
|
|
<!-- Left panel: Template list -->
|
|
<div class="w-[30%] border-r border-border-subtle overflow-y-auto bg-bg-surface" role="radiogroup" aria-label="Invoice templates">
|
|
<div v-for="cat in TEMPLATE_CATEGORIES" :key="cat.id">
|
|
<div
|
|
class="text-[0.5625rem] text-text-tertiary uppercase tracking-[0.08em] font-medium px-3 pt-3 pb-1"
|
|
>
|
|
{{ cat.label }}
|
|
</div>
|
|
<button
|
|
v-for="tmpl in getTemplatesByCategory(cat.id)"
|
|
:key="tmpl.id"
|
|
role="radio"
|
|
:aria-checked="tmpl.id === modelValue"
|
|
class="w-full flex items-center gap-2 px-3 py-1.5 text-[0.75rem] transition-colors"
|
|
:class="
|
|
tmpl.id === modelValue
|
|
? 'bg-accent/10 text-accent-text'
|
|
: 'text-text-secondary hover:bg-bg-elevated hover:text-text-primary'
|
|
"
|
|
@click="selectTemplate(tmpl.id)"
|
|
>
|
|
<span
|
|
class="w-2.5 h-2.5 rounded-full shrink-0 border border-black/10"
|
|
:style="{ backgroundColor: tmpl.colors.primary }"
|
|
aria-hidden="true"
|
|
/>
|
|
<span class="truncate">{{ tmpl.name }}</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right panel: Live preview -->
|
|
<div class="w-[70%] bg-bg-inset p-4 flex items-start justify-center overflow-y-auto" aria-label="Template preview" aria-live="polite">
|
|
<div class="w-full max-w-sm">
|
|
<InvoicePreview
|
|
:template="selectedTemplate"
|
|
:invoice="previewInvoice"
|
|
:client="previewClient"
|
|
:items="previewItems"
|
|
:business-info="previewBusinessInfo"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|