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:
@@ -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>
|
||||
Reference in New Issue
Block a user