Refund Request Form
Refunds are per line, not per order: a reason on each line that drives what is asked for next, and a running total itemising the restocking fee and the delivery rule.
A verdict for this serial number today, with exclusions given equal weight to cover and an extension offered only while it can still be bought.
components/warranty-coverage-panel.tsxnpx hoverlab add warranty-coverage-panel
Or over MCP, from your editor's agent — no account needed.
Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.
Start a page with this section — add more, order them, and leave with the page source.
Coverage is per unit, not per order. Pick the serial number on the underside of the product.
Covered until 12 March 2027
Kestrel sit-stand desk · serial KS-2291-8840 · bought 12 March 2025 · Standard · 2 years · 546 days remaining
This warranty is in addition to your statutory rights, which it does not affect. Nothing on this page shortens them.
Rendered live in your current theme — this is the same component whose source is below, not a screenshot of it.
'use client'
/**
* <WarrantyCoveragePanel> — what is covered, what is not, and until when.
*
* Warranty screens fail in one consistent way: they state a duration and stop.
* "2-year warranty" answers none of the three questions a customer opens the
* page with — is my specific unit still in it, does it cover the thing that
* has gone wrong, and what do I do now.
*
* SO THE PANEL IS BUILT AROUND A VERDICT
*
* A single line at the top says covered or not, for this serial number, today,
* with the date it lapses. Everything below is the evidence. A customer who
* reads only the first line has still been answered, which is the test any
* status surface should pass.
*
* WHAT IS *NOT* COVERED IS AS PROMINENT AS WHAT IS
*
* Two columns, equal weight. Hiding exclusions behind a link is how a
* warranty page turns into a complaint: the customer completes a claim, waits
* four days and is refused for something that was always written down. Stating
* it before the claim costs one column and saves the round trip.
*
* THE EXTENSION OFFER IS TIMED, NOT PERMANENT. It appears inside the window
* where an extension can still be bought and disappears afterwards, rather
* than sitting there permanently as an upsell — a lapsed warranty offering an
* extension is an insult, not a conversion.
*
* DATES ARE RENDERED IN A FIXED LOCALE AND TIME ZONE. `toLocaleDateString`
* with the browser's defaults produces one string on the server and another in
* the client, React discards the subtree, and the block flickers. The strings
* here are authored data for that reason — pass your own formatted values.
*
* ACCESSIBILITY: the verdict is a `role="status"` region rather than colour
* alone; the two coverage lists are separate `<h3>`-headed lists so a screen
* reader can jump between them; the serial-number picker is a labelled select
* because most households own more than one of anything.
*/
import * as React from 'react'
import { CircleCheck, CircleX, FileText, ShieldCheck, Wrench } from 'lucide-react'
export interface WarrantyUnit {
id: string
product: string
serial: string
purchased: string
expires: string
/** Days left. Zero or less renders as lapsed. */
daysLeft: number
plan: string
}
export interface WarrantyCoveragePanelProps {
units?: WarrantyUnit[]
covered?: string[]
excluded?: string[]
/** Extension is offered only inside this many days of expiry. */
extensionWindowDays?: number
className?: string
}
const DEFAULT_UNITS: WarrantyUnit[] = [
{
id: 'u1',
product: 'Kestrel sit-stand desk',
serial: 'KS-2291-8840',
purchased: '12 March 2025',
expires: '12 March 2027',
daysLeft: 546,
plan: 'Standard · 2 years',
},
{
id: 'u2',
product: 'Meridian task chair',
serial: 'MC-1187-2210',
purchased: '4 November 2024',
expires: '4 November 2026',
daysLeft: 52,
plan: 'Standard · 2 years',
},
{
id: 'u3',
product: 'Halo task lamp',
serial: 'HL-0042-9917',
purchased: '19 January 2023',
expires: '19 January 2025',
daysLeft: -602,
plan: 'Standard · 2 years',
},
]
const DEFAULT_COVERED = [
'Motor, control box and handset failure',
'Frame welds, joints and structural parts',
'Gas lift, castors and tilt mechanism',
'Manufacturing defects in the desktop surface',
'Labour and parts on an approved repair',
]
const DEFAULT_EXCLUDED = [
'Scratches, dents and finish wear from normal use',
'Damage from moving the unit without dismantling it',
'Units modified with third-party parts',
'Consumables — cable ties, felt pads, bulbs',
'Anything bought through an unauthorised reseller',
]
export function WarrantyCoveragePanel({
units = DEFAULT_UNITS,
covered = DEFAULT_COVERED,
excluded = DEFAULT_EXCLUDED,
extensionWindowDays = 90,
className = '',
}: WarrantyCoveragePanelProps) {
const uid = React.useId()
const [unitId, setUnitId] = React.useState(units[0]?.id ?? '')
const unit = units.find((u) => u.id === unitId) ?? units[0]
if (!unit) return null
const active = unit.daysLeft > 0
// Offered only where it is still buyable — see the docblock.
const canExtend = active && unit.daysLeft <= extensionWindowDays
return (
<section className={`bg-background px-4 py-10 sm:px-6 ${className}`}>
<div className="mx-auto max-w-3xl">
<div className="flex flex-wrap items-end justify-between gap-4">
<div>
<h2 className="text-xl font-bold tracking-tight">Warranty</h2>
<p className="mt-1 text-sm text-muted-foreground">
Coverage is per unit, not per order. Pick the serial number on the
underside of the product.
</p>
</div>
<div>
<label htmlFor={`${uid}-unit`} className="sr-only">
Choose a registered product
</label>
<select
id={`${uid}-unit`}
value={unitId}
onChange={(e) => setUnitId(e.target.value)}
className="rounded-lg border border-border bg-card px-3 py-2 text-sm outline-none focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-ring"
>
{units.map((u) => (
<option key={u.id} value={u.id}>
{u.product} · {u.serial}
</option>
))}
</select>
</div>
</div>
{/* The verdict. A customer who reads only this has been answered. */}
<div
role="status"
className={`mt-5 rounded-2xl border p-5 ${
active
? 'border-emerald-500/40 bg-emerald-500/5'
: 'border-border bg-muted/40'
}`}
>
<p className="flex items-center gap-2 text-base font-semibold">
{active ? (
<ShieldCheck aria-hidden className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
) : (
<CircleX aria-hidden className="h-5 w-5 text-muted-foreground" />
)}
{active
? `Covered until ${unit.expires}`
: `Cover lapsed on ${unit.expires}`}
</p>
<p className="mt-1.5 text-sm text-muted-foreground">
{unit.product} · serial <span className="font-mono text-xs">{unit.serial}</span>{' '}
· bought {unit.purchased} · {unit.plan}
{active ? ` · ${unit.daysLeft} days remaining` : ''}
</p>
<div className="mt-4 flex flex-wrap gap-2">
<button
type="button"
disabled={!active}
className="inline-flex items-center gap-1.5 rounded-lg bg-primary px-3 py-2 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
>
<Wrench aria-hidden className="h-4 w-4" />
Start a warranty claim
</button>
<a
href="#"
className="inline-flex items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<FileText aria-hidden className="h-4 w-4" />
Download the certificate
</a>
</div>
{canExtend ? (
<p className="mt-4 rounded-lg border border-border bg-card p-3 text-xs text-muted-foreground">
Cover ends in {unit.daysLeft} days. It can be extended by two years
for £49 up until the expiry date — not afterwards.{' '}
<a href="#" className="font-semibold text-primary underline-offset-2 hover:underline">
Extend this unit
</a>
</p>
) : null}
{!active ? (
<p className="mt-4 rounded-lg border border-border bg-card p-3 text-xs text-muted-foreground">
Out-of-warranty repairs are quoted before any work starts, and the
quote is free.{' '}
<a href="#" className="font-semibold text-primary underline-offset-2 hover:underline">
Request a repair quote
</a>
</p>
) : null}
</div>
{/* Equal weight, deliberately. */}
<div className="mt-6 grid gap-4 sm:grid-cols-2">
<div className="min-w-0 rounded-xl border border-border bg-card p-4">
<h3 data-stress-ignore className="text-sm font-semibold">Covered</h3>
<ul className="mt-3 space-y-2">
{covered.map((item) => (
<li key={item} className="flex gap-2 text-sm text-muted-foreground">
<CircleCheck
aria-hidden
className="mt-0.5 h-4 w-4 shrink-0 text-emerald-600 dark:text-emerald-400"
/>
<span className="min-w-0 flex-1 break-words">{item}</span>
</li>
))}
</ul>
</div>
<div className="min-w-0 rounded-xl border border-border bg-card p-4">
<h3 data-stress-ignore className="text-sm font-semibold">Not covered</h3>
<ul className="mt-3 space-y-2">
{excluded.map((item) => (
<li key={item} className="flex gap-2 text-sm text-muted-foreground">
<CircleX aria-hidden className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 break-words">{item}</span>
</li>
))}
</ul>
</div>
</div>
<p className="mt-4 text-xs text-muted-foreground">
This warranty is in addition to your statutory rights, which it does
not affect. Nothing on this page shortens them.
</p>
</div>
</section>
)
}
bg-card, text-muted-foreground) — it inherits your theme instead of overriding it.Drop it at components/warranty-coverage-panel.tsx and import it where you need the section:
import { WarrantyCoveragePanel } from '@/components/warranty-coverage-panel'1 of this block’s props are simple enough to drive from here. Change them and the block below re-renders — it is the same component whose source is above, not a mock of it. Everything else it accepts is in the table underneath.
Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.
| Prop | Type | Default |
|---|---|---|
units | WarrantyUnit[] | DEFAULT_UNITS |
covered | string[] | DEFAULT_COVERED |
excluded | string[] | DEFAULT_EXCLUDED |
extensionWindowDaysExtension is offered only inside this many days of expiry. | number | 90 |
className | string | '' |
The same block rendered once to markup, wrapped as a file your framework compiles. Tailwind classes are framework-agnostic, so the design transfers intact — the behaviour does not.
This block is interactive. The markup below is its initial state with the event handlers stripped — you will need to re-wire the behaviour in your framework.
<!--
Warranty Coverage Panel — markup from the Hoverlab catalog.
This is the block rendered once to HTML and wrapped as a component
file. It is not a port of the React source: the Tailwind classes carry
the design, which is the part that took the work, and they are the same
in every framework.
This block is interactive in React and the handlers are NOT here.
Buttons, toggles and menus render in their initial state and do
nothing until you wire them up.
-->
<section class="bg-background px-4 py-10 sm:px-6 ">
<div class="mx-auto max-w-3xl">
<div class="flex flex-wrap items-end justify-between gap-4">
<div>
<h2 class="text-xl font-bold tracking-tight">Warranty</h2>
<p class="mt-1 text-sm text-muted-foreground">Coverage is per unit, not per order. Pick the serial number on the underside of the product.</p>
</div>
<div>
<label for="_R_0_-unit" class="sr-only">Choose a registered product</label>
<select id="_R_0_-unit" class="rounded-lg border border-border bg-card px-3 py-2 text-sm outline-none focus-visible:border-primary focus-visible:ring-2 focus-visible:ring-ring">
<option value="u1" selected="">Kestrel sit-stand desk · KS-2291-8840</option>
<option value="u2">Meridian task chair · MC-1187-2210</option>
<option value="u3">Halo task lamp · HL-0042-9917</option>
</select>
</div>
</div>
<div role="status" class="mt-5 rounded-2xl border p-5 border-emerald-500/40 bg-emerald-500/5">
<p class="flex items-center gap-2 text-base font-semibold"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shield-check h-5 w-5 text-emerald-600 dark:text-emerald-400" aria-hidden="true"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"></path><path d="m9 12 2 2 4-4"></path></svg>Covered until 12 March 2027</p>
<p class="mt-1.5 text-sm text-muted-foreground">Kestrel sit-stand desk · serial <span class="font-mono text-xs">KS-2291-8840</span> · bought 12 March 2025 · Standard · 2 years · 546 days remaining</p>
<div class="mt-4 flex flex-wrap gap-2">
<button type="button" class="inline-flex items-center gap-1.5 rounded-lg bg-primary px-3 py-2 text-sm font-semibold text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-wrench h-4 w-4" aria-hidden="true"><path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"></path></svg>Start a warranty claim</button>
<a href="#" class="inline-flex items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-sm font-medium transition-colors hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-file-text h-4 w-4" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"></path><path d="M14 2v4a2 2 0 0 0 2 2h4"></path><path d="M10 9H8"></path><path d="M16 13H8"></path><path d="M16 17H8"></path></svg>Download the certificate</a>
</div>
</div>
<div class="mt-6 grid gap-4 sm:grid-cols-2">
<div class="min-w-0 rounded-xl border border-border bg-card p-4">
<h3 data-stress-ignore="true" class="text-sm font-semibold">Covered</h3>
<ul class="mt-3 space-y-2">
<li class="flex gap-2 text-sm text-muted-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check mt-0.5 h-4 w-4 shrink-0 text-emerald-600 dark:text-emerald-400" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
<path d="m9 12 2 2 4-4"></path>
</svg>
<span class="min-w-0 flex-1 break-words">Motor, control box and handset failure</span>
</li>
<li class="flex gap-2 text-sm text-muted-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check mt-0.5 h-4 w-4 shrink-0 text-emerald-600 dark:text-emerald-400" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
<path d="m9 12 2 2 4-4"></path>
</svg>
<span class="min-w-0 flex-1 break-words">Frame welds, joints and structural parts</span>
</li>
<li class="flex gap-2 text-sm text-muted-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check mt-0.5 h-4 w-4 shrink-0 text-emerald-600 dark:text-emerald-400" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
<path d="m9 12 2 2 4-4"></path>
</svg>
<span class="min-w-0 flex-1 break-words">Gas lift, castors and tilt mechanism</span>
</li>
<li class="flex gap-2 text-sm text-muted-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check mt-0.5 h-4 w-4 shrink-0 text-emerald-600 dark:text-emerald-400" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
<path d="m9 12 2 2 4-4"></path>
</svg>
<span class="min-w-0 flex-1 break-words">Manufacturing defects in the desktop surface</span>
</li>
<li class="flex gap-2 text-sm text-muted-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-check mt-0.5 h-4 w-4 shrink-0 text-emerald-600 dark:text-emerald-400" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
<path d="m9 12 2 2 4-4"></path>
</svg>
<span class="min-w-0 flex-1 break-words">Labour and parts on an approved repair</span>
</li>
</ul>
</div>
<div class="min-w-0 rounded-xl border border-border bg-card p-4">
<h3 data-stress-ignore="true" class="text-sm font-semibold">Not covered</h3>
<ul class="mt-3 space-y-2">
<li class="flex gap-2 text-sm text-muted-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
<path d="m15 9-6 6"></path>
<path d="m9 9 6 6"></path>
</svg>
<span class="min-w-0 flex-1 break-words">Scratches, dents and finish wear from normal use</span>
</li>
<li class="flex gap-2 text-sm text-muted-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
<path d="m15 9-6 6"></path>
<path d="m9 9 6 6"></path>
</svg>
<span class="min-w-0 flex-1 break-words">Damage from moving the unit without dismantling it</span>
</li>
<li class="flex gap-2 text-sm text-muted-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
<path d="m15 9-6 6"></path>
<path d="m9 9 6 6"></path>
</svg>
<span class="min-w-0 flex-1 break-words">Units modified with third-party parts</span>
</li>
<li class="flex gap-2 text-sm text-muted-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
<path d="m15 9-6 6"></path>
<path d="m9 9 6 6"></path>
</svg>
<span class="min-w-0 flex-1 break-words">Consumables — cable ties, felt pads, bulbs</span>
</li>
<li class="flex gap-2 text-sm text-muted-foreground">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-x mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" aria-hidden="true">
<circle cx="12" cy="12" r="10"></circle>
<path d="m15 9-6 6"></path>
<path d="m9 9 6 6"></path>
</svg>
<span class="min-w-0 flex-1 break-words">Anything bought through an unauthorised reseller</span>
</li>
</ul>
</div>
</div>
<p class="mt-4 text-xs text-muted-foreground">This warranty is in addition to your statutory rights, which it does not affect. Nothing on this page shortens them.</p>
</div>
</section>
What each framework gets across the whole catalog — effects convert properly; this rung is markup.
The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.
All of them free to read, copy and install — no account, no locked tiles, no watermarked preview. The whole catalog is open, and so are the API and the CLI.
Browse After-Sale ServiceCopying the code is free. Putting it in client work or a paid product is what Pro is for — the licence, not the access.
Refunds are per line, not per order: a reason on each line that drives what is asked for next, and a running total itemising the restocking fee and the delivery rule.
Where the money is and who is holding it - four stages, each naming the responsible party, including the bank stage every where-is-my-refund contact comes from.
The operator queue, sorted oldest-open-first because recency is the wrong default, with blocked as a first-class status that names its blocker.
Refund, exchange or credit decided per line, with live stock on the variants and the price difference shown before the goods are posted rather than after.
The screen between approved and posted: three drop-off methods with the thing that actually blocks each one, the packing rules that get returns refused, and a date rather than a duration.
A repair is not a return - a symptom picker that filters the service routes, warranty resolved inside the form, and an estimate with its uncertainty stated.
The reviews you wrote and the ones you owe, with editing that is honest about being marked as edited and moderation that names the rule it applied.
A balance checker built for the two ways it goes wrong - an empty card and an expired one - answering with a transaction history rather than a bare zero.