refactor(ui): form primitive polish + invoice page grid alignment (#475)
* refactor(ui): form primitive polish + invoice page grid alignment
Three things that were dropped from the previous design-refresh squash
and one new fix on invoices/[id].
Form primitives — tighter focus rings, match Button transition cadence:
- Input/Textarea/Select trigger: transition-colors duration-200 → 150
- Focus ring: border-primary/{50,60} + ring-primary/{20,35} →
border-primary + ring-primary/20 (full-opacity border with a
faint glow ring; cleaner than the opacity-stacked version)
- SelectContent: border-border/60 → border-border, shadow-md →
halved --shadow-md token
- SelectSeparator: bg-border/50 → bg-border
invoices/new — single grid with explicit row placement so the
second-row cards (Anteckningar / Summering) align across columns
regardless of how tall the first-row cards end up:
- Flatten the two column wrappers into one grid
- Add lg:auto-rows-min lg:items-start
- Each card pinned with lg:col-{start,span} + lg:row-start
- Fakturarader now spans col 1-3 (full width) on lg+
invoices/[id] — same alignment fix on the detail view:
- Flatten the column wrappers into one grid
- Kund (col 1-2) + Fakturarader (col 1-2) + Anteckningar (col 1-2,
conditional) pinned to rows 1-3
- All right-side cards (Detaljer + conditional status cards) stay
grouped in a single wrapper at lg:col-start-3 lg:row-span-3, so
they stack naturally without disrupting the left-column rows
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(ui): address PR review
- invoices/[id]: add lg:auto-rows-min to match invoices/new and prevent
unintended row expansion from any future auto-placed items
- Clean up stale indentation in both invoice pages — Card children
were 2 spaces too deep after the wrapper divs were removed
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -468,178 +468,175 @@ export default function InvoiceDetailPage({ params }: { params: Promise<{ id: st
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{/* Main content */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Customer info */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Kund</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium text-lg">{customer.name}</p>
|
||||
{customer.org_number && (
|
||||
<p className="text-muted-foreground">Org.nr: {customer.org_number}</p>
|
||||
<div className="grid gap-6 lg:grid-cols-3 lg:auto-rows-min lg:items-start">
|
||||
{/* Customer info */}
|
||||
<Card className="lg:col-span-2 lg:row-start-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Kund</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
<p className="font-medium text-lg">{customer.name}</p>
|
||||
{customer.org_number && (
|
||||
<p className="text-muted-foreground">Org.nr: {customer.org_number}</p>
|
||||
)}
|
||||
{customer.vat_number && (
|
||||
<p className="text-muted-foreground">VAT: {customer.vat_number}</p>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-4 pt-2 text-sm text-muted-foreground">
|
||||
{customer.email && (
|
||||
<span>{customer.email}</span>
|
||||
)}
|
||||
{customer.vat_number && (
|
||||
<p className="text-muted-foreground">VAT: {customer.vat_number}</p>
|
||||
{customer.phone && (
|
||||
<span>{customer.phone}</span>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-4 pt-2 text-sm text-muted-foreground">
|
||||
{customer.email && (
|
||||
<span>{customer.email}</span>
|
||||
)}
|
||||
{customer.phone && (
|
||||
<span>{customer.phone}</span>
|
||||
)}
|
||||
</div>
|
||||
{(customer.address_line1 || customer.city) && (
|
||||
<div className="text-sm text-muted-foreground pt-1">
|
||||
{customer.address_line1 && <p>{customer.address_line1}</p>}
|
||||
{customer.address_line2 && <p>{customer.address_line2}</p>}
|
||||
<p>
|
||||
{customer.postal_code} {customer.city}
|
||||
{customer.country !== 'SE' && `, ${customer.country}`}
|
||||
</p>
|
||||
</div>
|
||||
{(customer.address_line1 || customer.city) && (
|
||||
<div className="text-sm text-muted-foreground pt-1">
|
||||
{customer.address_line1 && <p>{customer.address_line1}</p>}
|
||||
{customer.address_line2 && <p>{customer.address_line2}</p>}
|
||||
<p>
|
||||
{customer.postal_code} {customer.city}
|
||||
{customer.country !== 'SE' && `, ${customer.country}`}
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Invoice items */}
|
||||
<Card className="lg:col-span-2 lg:row-start-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Fakturarader</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{/* Header — desktop */}
|
||||
<div className="hidden sm:grid grid-cols-12 gap-4 text-sm font-medium text-muted-foreground border-b pb-2">
|
||||
<div className="col-span-5">Beskrivning</div>
|
||||
<div className="col-span-2 text-right">Antal</div>
|
||||
<div className="col-span-1 text-center">Enhet</div>
|
||||
<div className="col-span-2 text-right">à-pris</div>
|
||||
<div className="col-span-2 text-right">Summa</div>
|
||||
</div>
|
||||
|
||||
{/* Items — desktop */}
|
||||
<div className="hidden sm:block space-y-4">
|
||||
{invoice.items.map((item) => (
|
||||
<div key={item.id} className="grid grid-cols-12 gap-4 text-sm">
|
||||
<div className="col-span-5">{item.description}</div>
|
||||
<div className="col-span-2 text-right">{item.quantity}</div>
|
||||
<div className="col-span-1 text-center">{item.unit}</div>
|
||||
<div className="col-span-2 text-right">
|
||||
{formatCurrency(item.unit_price, invoice.currency)}
|
||||
</div>
|
||||
<div className="col-span-2 text-right font-medium">
|
||||
{formatCurrency(item.line_total, invoice.currency)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Items — mobile cards */}
|
||||
<div className="sm:hidden space-y-2">
|
||||
{invoice.items.map((item) => (
|
||||
<div key={item.id} className="border rounded-lg p-3 text-sm space-y-1.5">
|
||||
<p className="font-medium">{item.description}</p>
|
||||
<div className="flex items-center justify-between text-muted-foreground">
|
||||
<span>{item.quantity} {item.unit} × {formatCurrency(item.unit_price, invoice.currency)}</span>
|
||||
</div>
|
||||
<p className="text-right font-medium">
|
||||
{formatCurrency(item.line_total, invoice.currency)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Invoice items */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Fakturarader</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{/* Header — desktop */}
|
||||
<div className="hidden sm:grid grid-cols-12 gap-4 text-sm font-medium text-muted-foreground border-b pb-2">
|
||||
<div className="col-span-5">Beskrivning</div>
|
||||
<div className="col-span-2 text-right">Antal</div>
|
||||
<div className="col-span-1 text-center">Enhet</div>
|
||||
<div className="col-span-2 text-right">à-pris</div>
|
||||
<div className="col-span-2 text-right">Summa</div>
|
||||
<Separator />
|
||||
|
||||
{/* Totals */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Delsumma</span>
|
||||
<span>{formatCurrency(invoice.subtotal, invoice.currency)}</span>
|
||||
</div>
|
||||
{(() => {
|
||||
const vatByRate = new Map<number, number>()
|
||||
for (const item of invoice.items) {
|
||||
const rate = item.vat_rate ?? 0
|
||||
const lineVat = Math.round(item.line_total * (rate / 100) * 100) / 100
|
||||
vatByRate.set(rate, (vatByRate.get(rate) || 0) + lineVat)
|
||||
}
|
||||
const entries = Array.from(vatByRate.entries())
|
||||
.filter(([, vat]) => vat > 0)
|
||||
.sort(([a], [b]) => b - a)
|
||||
|
||||
{/* Items — desktop */}
|
||||
<div className="hidden sm:block space-y-4">
|
||||
{invoice.items.map((item) => (
|
||||
<div key={item.id} className="grid grid-cols-12 gap-4 text-sm">
|
||||
<div className="col-span-5">{item.description}</div>
|
||||
<div className="col-span-2 text-right">{item.quantity}</div>
|
||||
<div className="col-span-1 text-center">{item.unit}</div>
|
||||
<div className="col-span-2 text-right">
|
||||
{formatCurrency(item.unit_price, invoice.currency)}
|
||||
</div>
|
||||
<div className="col-span-2 text-right font-medium">
|
||||
{formatCurrency(item.line_total, invoice.currency)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Items — mobile cards */}
|
||||
<div className="sm:hidden space-y-2">
|
||||
{invoice.items.map((item) => (
|
||||
<div key={item.id} className="border rounded-lg p-3 text-sm space-y-1.5">
|
||||
<p className="font-medium">{item.description}</p>
|
||||
<div className="flex items-center justify-between text-muted-foreground">
|
||||
<span>{item.quantity} {item.unit} × {formatCurrency(item.unit_price, invoice.currency)}</span>
|
||||
</div>
|
||||
<p className="text-right font-medium">
|
||||
{formatCurrency(item.line_total, invoice.currency)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* Totals */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Delsumma</span>
|
||||
<span>{formatCurrency(invoice.subtotal, invoice.currency)}</span>
|
||||
</div>
|
||||
{(() => {
|
||||
const vatByRate = new Map<number, number>()
|
||||
for (const item of invoice.items) {
|
||||
const rate = item.vat_rate ?? 0
|
||||
const lineVat = Math.round(item.line_total * (rate / 100) * 100) / 100
|
||||
vatByRate.set(rate, (vatByRate.get(rate) || 0) + lineVat)
|
||||
}
|
||||
const entries = Array.from(vatByRate.entries())
|
||||
.filter(([, vat]) => vat > 0)
|
||||
.sort(([a], [b]) => b - a)
|
||||
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Moms</span>
|
||||
<span>{formatCurrency(0, invoice.currency)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return entries.map(([rate, vat]) => (
|
||||
<div key={rate} className="flex justify-between">
|
||||
<span className="text-muted-foreground">Moms {rate}%</span>
|
||||
<span>{formatCurrency(vat, invoice.currency)}</span>
|
||||
</div>
|
||||
))
|
||||
})()}
|
||||
<Separator />
|
||||
{(() => {
|
||||
const rounding = getDisplayTotal(invoice, { ore_rounding: oreRounding })
|
||||
if (entries.length === 0) {
|
||||
return (
|
||||
<>
|
||||
{rounding.applies && (
|
||||
<div className="flex justify-between text-sm text-muted-foreground">
|
||||
<span>Öresavrundning</span>
|
||||
<span>{formatCurrency(rounding.roundingDelta, 'SEK')}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between font-bold text-lg">
|
||||
<span>Totalt</span>
|
||||
<span>{formatCurrency(rounding.displayed, invoice.currency)}</span>
|
||||
</div>
|
||||
</>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Moms</span>
|
||||
<span>{formatCurrency(0, invoice.currency)}</span>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
{invoice.currency !== 'SEK' && invoice.total_sek && (
|
||||
<div className="flex justify-between text-sm text-muted-foreground">
|
||||
<span>I SEK (kurs {invoice.exchange_rate})</span>
|
||||
<span>{formatCurrency(invoice.total_sek)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
}
|
||||
|
||||
{/* Notes */}
|
||||
{(invoice.notes || invoice.reverse_charge_text) && (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Anteckningar</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{invoice.reverse_charge_text && (
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<p className="text-sm font-medium">Omvänd skattskyldighet</p>
|
||||
<p className="text-sm text-muted-foreground">{invoice.reverse_charge_text}</p>
|
||||
return entries.map(([rate, vat]) => (
|
||||
<div key={rate} className="flex justify-between">
|
||||
<span className="text-muted-foreground">Moms {rate}%</span>
|
||||
<span>{formatCurrency(vat, invoice.currency)}</span>
|
||||
</div>
|
||||
))
|
||||
})()}
|
||||
<Separator />
|
||||
{(() => {
|
||||
const rounding = getDisplayTotal(invoice, { ore_rounding: oreRounding })
|
||||
return (
|
||||
<>
|
||||
{rounding.applies && (
|
||||
<div className="flex justify-between text-sm text-muted-foreground">
|
||||
<span>Öresavrundning</span>
|
||||
<span>{formatCurrency(rounding.roundingDelta, 'SEK')}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex justify-between font-bold text-lg">
|
||||
<span>Totalt</span>
|
||||
<span>{formatCurrency(rounding.displayed, invoice.currency)}</span>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
{invoice.currency !== 'SEK' && invoice.total_sek && (
|
||||
<div className="flex justify-between text-sm text-muted-foreground">
|
||||
<span>I SEK (kurs {invoice.exchange_rate})</span>
|
||||
<span>{formatCurrency(invoice.total_sek)}</span>
|
||||
</div>
|
||||
)}
|
||||
{invoice.notes && <p className="text-sm">{invoice.notes}</p>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Notes */}
|
||||
{(invoice.notes || invoice.reverse_charge_text) && (
|
||||
<Card className="lg:col-span-2 lg:row-start-3">
|
||||
<CardHeader>
|
||||
<CardTitle>Anteckningar</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{invoice.reverse_charge_text && (
|
||||
<div className="p-3 bg-muted rounded-lg">
|
||||
<p className="text-sm font-medium">Omvänd skattskyldighet</p>
|
||||
<p className="text-sm text-muted-foreground">{invoice.reverse_charge_text}</p>
|
||||
</div>
|
||||
)}
|
||||
{invoice.notes && <p className="text-sm">{invoice.notes}</p>}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
<div className="lg:col-start-3 lg:row-start-1 lg:row-span-3 space-y-6">
|
||||
{/* Invoice details */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
@@ -473,157 +473,117 @@ export default function NewInvoicePage() {
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6 pb-28 md:pb-0">
|
||||
<div className="grid gap-6 lg:grid-cols-3">
|
||||
{/* Main content */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
{/* Customer selection */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Kund<RequiredMark /></CardTitle>
|
||||
<CardDescription>Välj vilken kund fakturan ska skickas till</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Controller
|
||||
name="customer_id"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Välj kund" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{customers.map((customer) => (
|
||||
<SelectItem key={customer.id} value={customer.id}>
|
||||
{customer.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-2"
|
||||
onClick={() => setIsCreateCustomerOpen(true)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Skapa kund
|
||||
</Button>
|
||||
{errors.customer_id && (
|
||||
<p className="text-sm text-destructive mt-2">{errors.customer_id.message}</p>
|
||||
<div className="grid gap-6 lg:grid-cols-3 lg:auto-rows-min lg:items-start">
|
||||
{/* Customer selection */}
|
||||
<Card className="lg:col-span-2 lg:row-start-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Kund<RequiredMark /></CardTitle>
|
||||
<CardDescription>Välj vilken kund fakturan ska skickas till</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Controller
|
||||
name="customer_id"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Välj kund" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{customers.map((customer) => (
|
||||
<SelectItem key={customer.id} value={customer.id}>
|
||||
{customer.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-2"
|
||||
onClick={() => setIsCreateCustomerOpen(true)}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Skapa kund
|
||||
</Button>
|
||||
{errors.customer_id && (
|
||||
<p className="text-sm text-destructive mt-2">{errors.customer_id.message}</p>
|
||||
)}
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Invoice items */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Fakturarader</CardTitle>
|
||||
<CardDescription>Lägg till produkter eller tjänster</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{fields.map((field, index) => {
|
||||
const lineTotal = (watchItems[index]?.quantity || 0) * (watchItems[index]?.unit_price || 0)
|
||||
const lineVat = Math.round(lineTotal * (watchItems[index]?.vat_rate ?? 25) / 100 * 100) / 100
|
||||
return (
|
||||
<div
|
||||
key={field.id}
|
||||
className="rounded-lg border bg-card p-4 space-y-3 relative md:rounded-none md:border-0 md:bg-transparent md:p-0 md:space-y-0 md:grid md:grid-cols-12 md:gap-4 md:items-start"
|
||||
>
|
||||
{/* Description + mobile delete button */}
|
||||
<div className="flex items-start gap-2 md:contents">
|
||||
<div className="flex-1 space-y-1 md:col-span-3 md:space-y-2">
|
||||
<Label className="text-xs text-muted-foreground md:text-sm md:text-foreground">Beskrivning</Label>
|
||||
<Input
|
||||
placeholder="T.ex. Instagram-kampanj"
|
||||
{...register(`items.${index}.description`)}
|
||||
/>
|
||||
{errors.items?.[index]?.description && (
|
||||
<p className="text-sm text-destructive">
|
||||
{errors.items[index].description?.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0 min-h-[44px] min-w-[44px] -mr-2 -mt-1 md:hidden"
|
||||
onClick={() => remove(index)}
|
||||
disabled={fields.length === 1}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
{/* Invoice items */}
|
||||
<Card className="lg:col-span-3 lg:row-start-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Fakturarader</CardTitle>
|
||||
<CardDescription>Lägg till produkter eller tjänster</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{fields.map((field, index) => {
|
||||
const lineTotal = (watchItems[index]?.quantity || 0) * (watchItems[index]?.unit_price || 0)
|
||||
const lineVat = Math.round(lineTotal * (watchItems[index]?.vat_rate ?? 25) / 100 * 100) / 100
|
||||
return (
|
||||
<div
|
||||
key={field.id}
|
||||
className="rounded-lg border bg-card p-4 space-y-3 relative md:rounded-none md:border-0 md:bg-transparent md:p-0 md:space-y-0 md:grid md:grid-cols-12 md:gap-4 md:items-start"
|
||||
>
|
||||
{/* Description + mobile delete button */}
|
||||
<div className="flex items-start gap-2 md:contents">
|
||||
<div className="flex-1 space-y-1 md:col-span-3 md:space-y-2">
|
||||
<Label className="text-xs text-muted-foreground md:text-sm md:text-foreground">Beskrivning</Label>
|
||||
<Input
|
||||
placeholder="T.ex. Instagram-kampanj"
|
||||
{...register(`items.${index}.description`)}
|
||||
/>
|
||||
{errors.items?.[index]?.description && (
|
||||
<p className="text-sm text-destructive">
|
||||
{errors.items[index].description?.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="shrink-0 min-h-[44px] min-w-[44px] -mr-2 -mt-1 md:hidden"
|
||||
onClick={() => remove(index)}
|
||||
disabled={fields.length === 1}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Antal, Enhet, à-pris */}
|
||||
<div className="grid grid-cols-3 gap-2 md:contents">
|
||||
<div className="space-y-1 md:col-span-2 md:space-y-2">
|
||||
<Label className="text-xs text-muted-foreground md:text-sm md:text-foreground">Antal</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
inputMode="decimal"
|
||||
className="text-right tabular-nums"
|
||||
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 md:col-span-2 md:space-y-2">
|
||||
<Label className="text-xs text-muted-foreground md:text-sm md:text-foreground">Enhet</Label>
|
||||
<Controller
|
||||
name={`items.${index}.unit`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{units.map((unit) => (
|
||||
<SelectItem key={unit} value={unit}>
|
||||
{unit}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 md:col-span-2 md:space-y-2">
|
||||
<Label className="text-xs text-muted-foreground md:text-sm md:text-foreground">à-pris</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="any"
|
||||
inputMode="decimal"
|
||||
className="text-right tabular-nums"
|
||||
{...register(`items.${index}.unit_price`, { valueAsNumber: true })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Moms */}
|
||||
{/* Antal, Enhet, à-pris */}
|
||||
<div className="grid grid-cols-3 gap-2 md:contents">
|
||||
<div className="space-y-1 md:col-span-2 md:space-y-2">
|
||||
<Label className="text-xs text-muted-foreground md:text-sm md:text-foreground">Moms</Label>
|
||||
<Label className="text-xs text-muted-foreground md:text-sm md:text-foreground">Antal</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
inputMode="decimal"
|
||||
className="text-right tabular-nums"
|
||||
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1 md:col-span-2 md:space-y-2">
|
||||
<Label className="text-xs text-muted-foreground md:text-sm md:text-foreground">Enhet</Label>
|
||||
<Controller
|
||||
name={`items.${index}.vat_rate`}
|
||||
name={`items.${index}.unit`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={String(field.value ?? 25)}
|
||||
onValueChange={(v) => field.onChange(Number(v))}
|
||||
disabled={isRateLocked}
|
||||
>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableRates.map((opt) => (
|
||||
<SelectItem key={opt.rate} value={String(opt.rate)}>
|
||||
{opt.label}
|
||||
{units.map((unit) => (
|
||||
<SelectItem key={unit} value={unit}>
|
||||
{unit}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
@@ -631,214 +591,248 @@ export default function NewInvoicePage() {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Desktop delete button */}
|
||||
<div className="hidden md:flex md:col-span-1 md:items-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => remove(index)}
|
||||
disabled={fields.length === 1}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Mobile summary row */}
|
||||
<div className="flex justify-between text-sm pt-1 border-t border-border/40 md:hidden">
|
||||
<span className="text-muted-foreground">Rad {index + 1}</span>
|
||||
<span className="font-medium tabular-nums">{formatCurrency(lineTotal + lineVat, watchCurrency)}</span>
|
||||
<div className="space-y-1 md:col-span-2 md:space-y-2">
|
||||
<Label className="text-xs text-muted-foreground md:text-sm md:text-foreground">à-pris</Label>
|
||||
<Input
|
||||
type="number"
|
||||
step="any"
|
||||
inputMode="decimal"
|
||||
className="text-right tabular-nums"
|
||||
{...register(`items.${index}.unit_price`, { valueAsNumber: true })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full md:w-auto"
|
||||
onClick={() =>
|
||||
append({ description: '', quantity: 1, unit: 'st', unit_price: 0, vat_rate: availableRates[0]?.rate ?? 25 })
|
||||
}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Lägg till rad
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{/* Moms */}
|
||||
<div className="space-y-1 md:col-span-2 md:space-y-2">
|
||||
<Label className="text-xs text-muted-foreground md:text-sm md:text-foreground">Moms</Label>
|
||||
<Controller
|
||||
name={`items.${index}.vat_rate`}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={String(field.value ?? 25)}
|
||||
onValueChange={(v) => field.onChange(Number(v))}
|
||||
disabled={isRateLocked}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{availableRates.map((opt) => (
|
||||
<SelectItem key={opt.rate} value={String(opt.rate)}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Notes */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Anteckningar</CardTitle>
|
||||
<CardDescription>Valfritt meddelande på fakturan</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
placeholder="T.ex. betalningsvillkor eller tack för samarbetet..."
|
||||
{...register('notes')}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
{/* Desktop delete button */}
|
||||
<div className="hidden md:flex md:col-span-1 md:items-end">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => remove(index)}
|
||||
disabled={fields.length === 1}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="space-y-6">
|
||||
{/* Invoice details */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Fakturadetaljer</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Dokumenttyp</Label>
|
||||
<Controller
|
||||
name="document_type"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="invoice">Faktura</SelectItem>
|
||||
<SelectItem value="proforma">Proformafaktura</SelectItem>
|
||||
<SelectItem value="delivery_note">Följesedel</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Valuta</Label>
|
||||
<Controller
|
||||
name="currency"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{currencies.map((currency) => (
|
||||
<SelectItem key={currency} value={currency}>
|
||||
{currency}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Fakturadatum<RequiredMark /></Label>
|
||||
<Input type="date" {...register('invoice_date')} aria-required="true" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Förfallodatum<RequiredMark /></Label>
|
||||
<Input type="date" {...register('due_date')} aria-required="true" />
|
||||
</div>
|
||||
|
||||
{watchDocumentType === 'invoice' && (
|
||||
<div className="space-y-2">
|
||||
<Label>Leveransdatum</Label>
|
||||
<Input type="date" {...register('delivery_date')} placeholder="Om det skiljer sig från fakturadatum" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Er referens</Label>
|
||||
<Controller
|
||||
name="your_reference"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TagInput
|
||||
value={field.value ?? ''}
|
||||
onChange={field.onChange}
|
||||
placeholder="Kontaktperson hos kund"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Vår referens</Label>
|
||||
<Controller
|
||||
name="our_reference"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TagInput
|
||||
value={field.value ?? ''}
|
||||
onChange={field.onChange}
|
||||
placeholder="Ditt namn"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Summary */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Summering</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Delsumma</span>
|
||||
<span>{formatCurrency(subtotal, watchCurrency)}</span>
|
||||
</div>
|
||||
{Array.from(vatByRate.entries())
|
||||
.sort(([a], [b]) => b - a)
|
||||
.map(([rate, group]) => (
|
||||
<div key={rate}>
|
||||
{vatByRate.size > 1 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Netto {rate}%</span>
|
||||
<span>{formatCurrency(group.base, watchCurrency)}</span>
|
||||
</div>
|
||||
)}
|
||||
{group.vat > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Moms {rate}%</span>
|
||||
<span>{formatCurrency(group.vat, watchCurrency)}</span>
|
||||
</div>
|
||||
)}
|
||||
{/* Mobile summary row */}
|
||||
<div className="flex justify-between text-sm pt-1 border-t border-border/40 md:hidden">
|
||||
<span className="text-muted-foreground">Rad {index + 1}</span>
|
||||
<span className="font-medium tabular-nums">{formatCurrency(lineTotal + lineVat, watchCurrency)}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{vatByRate.size === 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Moms</span>
|
||||
<span>{formatCurrency(0, watchCurrency)}</span>
|
||||
</div>
|
||||
)}
|
||||
<Separator />
|
||||
<div className="flex justify-between font-bold text-lg">
|
||||
<span>Totalt</span>
|
||||
<span>{formatCurrency(total, watchCurrency)}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})}
|
||||
|
||||
{/* Actions — desktop/tablet only */}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full hidden md:block"
|
||||
size="lg"
|
||||
disabled={isSubmitting || !canWrite}
|
||||
title={!canWrite ? 'Du har endast läsbehörighet i detta företag' : undefined}
|
||||
>
|
||||
{!canWrite && <Lock className="mr-2 h-4 w-4 inline" />}
|
||||
Granska & skapa
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full md:w-auto"
|
||||
onClick={() =>
|
||||
append({ description: '', quantity: 1, unit: 'st', unit_price: 0, vat_rate: availableRates[0]?.rate ?? 25 })
|
||||
}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
Lägg till rad
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Notes */}
|
||||
<Card className="lg:col-span-2 lg:row-start-3">
|
||||
<CardHeader>
|
||||
<CardTitle>Anteckningar</CardTitle>
|
||||
<CardDescription>Valfritt meddelande på fakturan</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Textarea
|
||||
placeholder="T.ex. betalningsvillkor eller tack för samarbetet..."
|
||||
{...register('notes')}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Invoice details */}
|
||||
<Card className="lg:col-start-3 lg:row-start-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Fakturadetaljer</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Dokumenttyp</Label>
|
||||
<Controller
|
||||
name="document_type"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="invoice">Faktura</SelectItem>
|
||||
<SelectItem value="proforma">Proformafaktura</SelectItem>
|
||||
<SelectItem value="delivery_note">Följesedel</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Valuta</Label>
|
||||
<Controller
|
||||
name="currency"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{currencies.map((currency) => (
|
||||
<SelectItem key={currency} value={currency}>
|
||||
{currency}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Fakturadatum<RequiredMark /></Label>
|
||||
<Input type="date" {...register('invoice_date')} aria-required="true" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Förfallodatum<RequiredMark /></Label>
|
||||
<Input type="date" {...register('due_date')} aria-required="true" />
|
||||
</div>
|
||||
|
||||
{watchDocumentType === 'invoice' && (
|
||||
<div className="space-y-2">
|
||||
<Label>Leveransdatum</Label>
|
||||
<Input type="date" {...register('delivery_date')} placeholder="Om det skiljer sig från fakturadatum" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Er referens</Label>
|
||||
<Controller
|
||||
name="your_reference"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TagInput
|
||||
value={field.value ?? ''}
|
||||
onChange={field.onChange}
|
||||
placeholder="Kontaktperson hos kund"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Vår referens</Label>
|
||||
<Controller
|
||||
name="our_reference"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<TagInput
|
||||
value={field.value ?? ''}
|
||||
onChange={field.onChange}
|
||||
placeholder="Ditt namn"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Summary */}
|
||||
<Card className="lg:col-start-3 lg:row-start-3">
|
||||
<CardHeader>
|
||||
<CardTitle>Summering</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Delsumma</span>
|
||||
<span>{formatCurrency(subtotal, watchCurrency)}</span>
|
||||
</div>
|
||||
{Array.from(vatByRate.entries())
|
||||
.sort(([a], [b]) => b - a)
|
||||
.map(([rate, group]) => (
|
||||
<div key={rate}>
|
||||
{vatByRate.size > 1 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Netto {rate}%</span>
|
||||
<span>{formatCurrency(group.base, watchCurrency)}</span>
|
||||
</div>
|
||||
)}
|
||||
{group.vat > 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Moms {rate}%</span>
|
||||
<span>{formatCurrency(group.vat, watchCurrency)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{vatByRate.size === 0 && (
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Moms</span>
|
||||
<span>{formatCurrency(0, watchCurrency)}</span>
|
||||
</div>
|
||||
)}
|
||||
<Separator />
|
||||
<div className="flex justify-between font-bold text-lg">
|
||||
<span>Totalt</span>
|
||||
<span>{formatCurrency(total, watchCurrency)}</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Actions — desktop/tablet only */}
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full hidden md:block lg:col-start-3 lg:row-start-4"
|
||||
size="lg"
|
||||
disabled={isSubmitting || !canWrite}
|
||||
title={!canWrite ? 'Du har endast läsbehörighet i detta företag' : undefined}
|
||||
>
|
||||
{!canWrite && <Lock className="mr-2 h-4 w-4 inline" />}
|
||||
Granska & skapa
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Mobile sticky total bar */}
|
||||
|
||||
@@ -9,7 +9,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-lg border border-input bg-card px-4 py-2 text-sm transition-colors duration-200 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground/60 focus-visible:outline-none focus-visible:border-primary/60 focus-visible:ring-1 focus-visible:ring-primary/35 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-10 w-full rounded-lg border border-input bg-card px-4 py-2 text-sm transition-colors duration-150 file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground/60 focus-visible:outline-none focus-visible:border-primary focus-visible:ring-1 focus-visible:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -18,7 +18,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-lg border border-input bg-card px-4 py-2 text-sm transition-colors duration-200 placeholder:text-muted-foreground/60 focus:outline-none focus:border-primary/60 focus:ring-1 focus:ring-primary/35 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
"flex h-10 w-full items-center justify-between rounded-lg border border-input bg-card px-4 py-2 text-sm transition-colors duration-150 placeholder:text-muted-foreground/60 focus:outline-none focus:border-primary focus:ring-1 focus:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -74,7 +74,7 @@ const SelectContent = React.forwardRef<
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg border border-border/60 bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg border border-border bg-popover text-popover-foreground shadow-[var(--shadow-md)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className
|
||||
@@ -138,7 +138,7 @@ const SelectSeparator = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-border/50", className)}
|
||||
className={cn("-mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
|
||||
@@ -8,7 +8,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
return (
|
||||
<textarea
|
||||
className={cn(
|
||||
"flex min-h-[100px] w-full rounded-lg border border-input bg-card px-4 py-3 text-sm transition-colors duration-200 placeholder:text-muted-foreground/60 focus-visible:outline-none focus-visible:border-primary/50 focus-visible:ring-1 focus-visible:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50 resize-none",
|
||||
"flex min-h-[100px] w-full rounded-lg border border-input bg-card px-4 py-3 text-sm transition-colors duration-150 placeholder:text-muted-foreground/60 focus-visible:outline-none focus-visible:border-primary focus-visible:ring-1 focus-visible:ring-primary/20 disabled:cursor-not-allowed disabled:opacity-50 resize-none",
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
Reference in New Issue
Block a user