feat(frontend): add new UI components and utilities

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-06-18 16:26:56 +01:00
co-authored by Sisyphus
parent 3ff14fdd5a
commit 165d3d6c38
7 changed files with 508 additions and 0 deletions
@@ -0,0 +1,87 @@
<script lang="ts">
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Checkbox } from '$lib/components/ui/checkbox';
let {
cardNumber = $bindable(''),
cardExpiry = $bindable(''),
cardCVC = $bindable(''),
saveCard = $bindable(false),
showSaveCard = false,
disabled = false
}: {
cardNumber?: string;
cardExpiry?: string;
cardCVC?: string;
saveCard?: boolean;
showSaveCard?: boolean;
disabled?: boolean;
} = $props();
function formatNumber(value: string): string {
const digits = value.replace(/\D/g, '').substring(0, 16);
const groups = digits.match(/.{1,4}/g);
return groups ? groups.join(' ') : digits;
}
function formatExpiry(value: string): string {
const digits = value.replace(/\D/g, '').substring(0, 4);
if (digits.length >= 3) {
return digits.substring(0, 2) + '/' + digits.substring(2);
}
return digits;
}
</script>
<div class="rounded-lg border border-gray-100 bg-gray-50 p-4">
<h4 class="mb-4 text-sm font-medium text-gray-700">Card Details</h4>
<div class="space-y-4">
<div class="space-y-2">
<Label for="cardNumber">Card Number</Label>
<Input
id="cardNumber"
type="text"
inputmode="numeric"
value={cardNumber}
oninput={(e) => (cardNumber = formatNumber((e.target as HTMLInputElement).value))}
placeholder="1234 5678 9012 3456"
maxlength={19}
{disabled}
/>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="cardExpiry">Expiry (MM/YY)</Label>
<Input
id="cardExpiry"
type="text"
inputmode="numeric"
value={cardExpiry}
oninput={(e) => (cardExpiry = formatExpiry((e.target as HTMLInputElement).value))}
placeholder="MM/YY"
maxlength={5}
{disabled}
/>
</div>
<div class="space-y-2">
<Label for="cardCVC">CVC</Label>
<Input
id="cardCVC"
type="text"
inputmode="numeric"
bind:value={cardCVC}
placeholder="123"
maxlength={4}
{disabled}
/>
</div>
</div>
{#if showSaveCard}
<div class="flex items-center gap-2">
<Checkbox id="saveCard" bind:checked={saveCard} disabled={disabled} />
<Label for="saveCard" class="text-sm font-normal">Save card for next time</Label>
</div>
{/if}
</div>
</div>
@@ -0,0 +1,16 @@
<script lang="ts">
let { status }: { status: string } = $props();
const variantMap: Record<string, { label: string; variantClass: string }> = {
draft: { label: 'Draft', variantClass: 'bg-gray-100 text-gray-800' },
active: { label: 'Active', variantClass: 'bg-emerald-100 text-emerald-800' },
completed: { label: 'Completed', variantClass: 'bg-blue-100 text-blue-800' },
cancelled: { label: 'Cancelled', variantClass: 'bg-red-100 text-red-800' }
};
const display = $derived(variantMap[status] ?? variantMap.draft);
</script>
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium {display.variantClass}">
{display.label}
</span>
@@ -0,0 +1,68 @@
<script lang="ts">
import type { Snippet } from 'svelte';
let {
trigger
}: {
trigger?: Snippet;
} = $props();
let open = $state(false);
function toggle() {
open = !open;
}
function handleClickOutside(e: MouseEvent) {
const target = e.target as HTMLElement;
if (!target.closest('[data-policy-popover]')) {
open = false;
}
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') open = false;
}
</script>
<svelte:window onclick={handleClickOutside} onkeydown={handleKeydown} />
<span data-policy-popover class="relative inline-block">
<button
type="button"
onclick={toggle}
class="inline underline cursor-pointer text-xs hover:text-amber-700"
>
{#if trigger}
{@render trigger()}
{:else}
cancellation policy
{/if}
</button>
{#if open}
<div
class="absolute left-0 top-full z-50 mt-1 w-48 rounded-md border border-gray-200 bg-white p-2 shadow-lg"
>
<a
href="/cancellation-policy"
target="_blank"
class="block w-full rounded px-3 py-2 text-left text-sm hover:bg-gray-100"
onclick={() => (open = false)}
>
Open
</a>
<button
type="button"
class="block w-full rounded px-3 py-2 text-left text-sm hover:bg-gray-100"
onclick={() => {
open = false;
const win = window.open('/cancellation-policy?format=pdf', '_blank');
if (win) win.focus();
}}
>
Download PDF
</button>
</div>
{/if}
</span>