Files
gw-svelte/src/lib/components/ui/ModalShell.svelte
T
adminandClaude Opus 4.8 0e88e7bdac v4.0.5
Component architecture
- Reorganise src/lib/components into pages/, sections/, ui/ subdirectories and
  update all imports across routes and tests.

Owner admin dashboard (+2k lines)
- Scheduled welcome-pack emails: durable queue with cancel/reschedule and a
  background sender loop (SCHEDULED_CHECK_INTERVAL_SECONDS, default 60s).
- Custom welcome-pack subject line, preview recipients, and client BCC.
- Add-client, edit client profile, and reset-onboarding flows.
- "View as client" onboarding preview (owner impersonation, dry-run submit).
- Tabbed owner welcome route (/owner/welcome/[[tab]]).

MYOB integration
- New mail_api/myob.py: create new clients as MYOB AccountRight customer
  contacts. Disabled until all MYOB_* env vars are set (no-op otherwise).

Onboarding
- New "Does your dog resource guard?" Yes/No question in the Behaviour step.
- Persist vetAddress, flea/tick, and pet-insurance fields server-side.

Misc
- vite config, new hooks.ts, responsive CSS tweaks, deploy/docker config,
  mail-api README and start-dev.ps1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 21:51:29 +12:00

123 lines
2.5 KiB
Svelte

<script lang="ts">
import { onMount } from 'svelte';
export let onClose: () => void;
export let ariaLabel: string | undefined = undefined;
export let ariaLabelledBy: string | undefined = undefined;
let hasMounted = false;
function portalToBody(node: HTMLElement) {
document.body.appendChild(node);
return {
destroy() {
node.remove();
}
};
}
function handleKeydown(event: KeyboardEvent) {
if (event.key === 'Escape') onClose();
}
onMount(() => {
hasMounted = true;
});
</script>
{#if hasMounted}
<div
use:portalToBody
class="modal-backdrop"
role="dialog"
aria-modal="true"
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
on:click|self={onClose}
on:keydown={handleKeydown}
tabindex="-1"
>
<div class="modal-card">
<button class="modal-close" type="button" aria-label="Close" on:click={onClose}>
&#x2715;
</button>
<slot />
</div>
</div>
{/if}
<style>
.modal-backdrop {
position: fixed;
inset: 0;
z-index: 1000;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background: rgba(10, 20, 10, 0.55);
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
animation: backdrop-in 0.25s ease;
}
.modal-card {
position: relative;
width: 100%;
max-width: 480px;
padding: 52px 48px 44px;
background: #fff;
border-radius: 24px;
box-shadow: 0 24px 80px rgba(10, 20, 10, 0.22);
text-align: center;
animation: card-in 0.35s cubic-bezier(0.22, 1, 0.36, 1);
}
.modal-close {
position: absolute;
top: 18px;
right: 20px;
width: 32px;
height: 32px;
border: none;
border-radius: 50%;
background: #f2f2f0;
color: #888;
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition:
background 0.15s ease,
color 0.15s ease;
}
.modal-close:hover {
background: #e8e8e4;
color: #333;
}
@keyframes backdrop-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes card-in {
from { opacity: 0; transform: scale(0.88) translateY(16px); }
to { opacity: 1; transform: scale(1) translateY(0); }
}
@keyframes bounce-in {
from { opacity: 0; transform: scale(0.4); }
to { opacity: 1; transform: scale(1); }
}
@media (max-width: 480px) {
.modal-card {
padding: 44px 28px 32px;
}
}
</style>