feat: add form and layout components to shared-ui (Tier 6b)

New molecules:
- ModalFooter: Standardized modal footer with button layout
- DataCard: Generic card for displaying data items (memos, decks, etc.)
- PageHeader: Standardized page header with title, description, actions

New organisms:
- FormModal: Modal with built-in form handling, validation, loading states

All components use Svelte 5 snippets for flexible slot patterns.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-11-25 00:36:12 +01:00
parent 3c457f9c18
commit 5045d70bf7
7 changed files with 505 additions and 1 deletions

View file

@ -0,0 +1,117 @@
<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}>
<form onsubmit={handleSubmit} onkeydown={handleKeydown} class="space-y-4">
<!-- Error message -->
{#if error}
<div class="rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 p-3">
<Text variant="small" class="text-red-600 dark:text-red-400">
{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>

View file

@ -1,4 +1,5 @@
export { default as Modal } from './Modal.svelte';
export { default as ConfirmationModal } from './ConfirmationModal.svelte';
export { default as FormModal } from './FormModal.svelte';
export { default as AppSlider } from './AppSlider.svelte';
export type { AppItem } from './AppSlider.types';