managarten/packages/shared-ui/src/organisms/FormModal.svelte
Till-JS bd89871f8b feat(ui): add elevation system for overlays and modals
- Add 3-level elevation CSS variables to themes.css for all theme variants
- elevation-1: dropdowns, pills (16% in dark mode)
- elevation-2: modals, overlays (20% in dark mode)
- elevation-3: context menus, tooltips (24% in dark mode)
- Update ContextMenu to use elevation-3
- Update Modal to use elevation-2 with theme-aware borders
- Update QuickEventOverlay to use elevation-2 with matching footer
- Update PillTimeRangeSelector dropdown to use elevation-1
- Update ConfirmationModal and FormModal to use theme variables
- Remove shadows from overlay components for cleaner look

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-13 15:00:33 +01:00

118 lines
2.7 KiB
Svelte

<script lang="ts">
/**
* FormModal - Modal with built-in form handling
*
* Extends Modal with form submission, validation error display,
* and standardized footer buttons.
*
* @example Basic form modal
* ```svelte
* <FormModal
* visible={showModal}
* onClose={() => showModal = false}
* onSubmit={handleSubmit}
* title="Create Item"
* submitLabel="Create"
* >
* <Input label="Name" bind:value={name} />
* <Textarea label="Description" bind:value={description} />
* </FormModal>
* ```
*/
import type { Snippet } from 'svelte';
import Modal from './Modal.svelte';
import { Button, Text } from '../atoms';
type SubmitVariant = 'primary' | 'danger' | 'success';
interface Props {
/** Whether the modal is visible */
visible: boolean;
/** Called when modal is closed */
onClose: () => void;
/** Called when form is submitted */
onSubmit: () => void | Promise<void>;
/** Modal title */
title: string;
/** Form content */
children: Snippet;
/** Icon snippet for header */
icon?: Snippet;
/** Submit button label */
submitLabel?: string;
/** Cancel button label */
cancelLabel?: string;
/** Submit button variant */
submitVariant?: SubmitVariant;
/** Whether submission is in progress */
loading?: boolean;
/** Error message to display */
error?: string | null;
/** Max width of modal */
maxWidth?: 'sm' | 'md' | 'lg' | 'xl';
/** Whether submit button is disabled */
submitDisabled?: boolean;
}
let {
visible,
onClose,
onSubmit,
title,
children,
icon,
submitLabel = 'Submit',
cancelLabel = 'Cancel',
submitVariant = 'primary',
loading = false,
error = null,
maxWidth = 'md',
submitDisabled = false,
}: Props = $props();
async function handleSubmit(e: Event) {
e.preventDefault();
await onSubmit();
}
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter' && e.ctrlKey && !loading && !submitDisabled) {
handleSubmit(e);
}
}
</script>
<Modal {visible} {onClose} {title} {icon} {maxWidth}>
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<form onsubmit={handleSubmit} onkeydown={handleKeydown} class="space-y-4">
<!-- Error message -->
{#if error}
<div class="rounded-lg bg-error/10 border border-error/30 p-3">
<Text variant="small" class="text-error">
{error}
</Text>
</div>
{/if}
<!-- Form content -->
{@render children()}
</form>
{#snippet footer()}
<div class="flex gap-3 justify-end">
<Button variant="ghost" onclick={onClose} disabled={loading}>
{cancelLabel}
</Button>
<Button
variant={submitVariant}
type="submit"
onclick={handleSubmit}
{loading}
disabled={submitDisabled}
>
{submitLabel}
</Button>
</div>
{/snippet}
</Modal>