refactor: merge FormField and FormFieldWrapper components

- Add children prop to FormField to support wrapper mode
- Update components using FormFieldWrapper to use FormField
- Remove redundant FormFieldWrapper component
- Maintain all existing functionality with unified API

This reduces code duplication and simplifies the form component hierarchy.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Justin Edmund 2025-06-25 22:14:10 -04:00
parent ea7ec61377
commit 05ddafcdea
5 changed files with 37 additions and 95 deletions

View file

@ -97,7 +97,7 @@ Refactor components to reduce duplication and complexity.
- [-] **Create base components** - [-] **Create base components**
- [x] Extract `BaseModal` component for shared modal logic - [x] Extract `BaseModal` component for shared modal logic
- [x] Create `BaseDropdown` for dropdown patterns - [x] Create `BaseDropdown` for dropdown patterns
- [ ] Merge `FormField` and `FormFieldWrapper` - [x] Merge `FormField` and `FormFieldWrapper`
- [ ] Create `BaseSegmentedController` for shared logic - [ ] Create `BaseSegmentedController` for shared logic
- [ ] **Refactor photo grids** - [ ] **Refactor photo grids**

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
label: string label: string
name: string name?: string
type?: string type?: string
value?: any value?: any
placeholder?: string placeholder?: string
@ -10,11 +10,12 @@
helpText?: string helpText?: string
disabled?: boolean disabled?: boolean
onchange?: (e: Event) => void onchange?: (e: Event) => void
children?: any
} }
let { let {
label, label,
name, name = '',
type = 'text', type = 'text',
value = $bindable(), value = $bindable(),
placeholder = '', placeholder = '',
@ -22,9 +23,13 @@
error = '', error = '',
helpText = '', helpText = '',
disabled = false, disabled = false,
onchange onchange,
children
}: Props = $props() }: Props = $props()
// If children are provided, this is a wrapper mode
const isWrapper = $derived(!!children)
function handleChange(e: Event) { function handleChange(e: Event) {
const target = e.target as HTMLInputElement | HTMLTextAreaElement const target = e.target as HTMLInputElement | HTMLTextAreaElement
value = target.value value = target.value
@ -33,14 +38,16 @@
</script> </script>
<div class="form-field" class:has-error={!!error}> <div class="form-field" class:has-error={!!error}>
<label for={name}> <label for={!isWrapper ? name : undefined}>
{label} {label}
{#if required} {#if required}
<span class="required">*</span> <span class="required">*</span>
{/if} {/if}
</label> </label>
{#if type === 'textarea'} {#if isWrapper}
{@render children()}
{:else if type === 'textarea'}
<textarea <textarea
id={name} id={name}
{name} {name}
@ -75,10 +82,17 @@
.form-field { .form-field {
margin-bottom: $unit-4x; margin-bottom: $unit-4x;
&:last-child {
margin-bottom: 0;
}
&.has-error { &.has-error {
input, input,
textarea { textarea,
border-color: #c33; :global(input),
:global(textarea),
:global(select) {
border-color: $red-error;
} }
} }
} }
@ -88,9 +102,10 @@
margin-bottom: $unit; margin-bottom: $unit;
font-weight: 500; font-weight: 500;
color: $gray-20; color: $gray-20;
font-size: 0.925rem;
.required { .required {
color: #c33; color: $red-error;
margin-left: 2px; margin-left: 2px;
} }
} }
@ -100,11 +115,11 @@
width: 100%; width: 100%;
padding: $unit-2x $unit-3x; padding: $unit-2x $unit-3x;
border: 1px solid $gray-80; border: 1px solid $gray-80;
border-radius: 6px; border-radius: $corner-radius-sm;
font-size: 1rem; font-size: 1rem;
font-family: inherit; font-family: inherit;
transition: border-color 0.2s ease; transition: border-color $transition-normal ease;
background-color: white; background-color: $white;
&:focus { &:focus {
outline: none; outline: none;
@ -124,13 +139,13 @@
.error-text { .error-text {
margin-top: $unit; margin-top: $unit;
color: #c33; color: $red-error;
font-size: 0.875rem; font-size: $font-size-small;
} }
.help-text { .help-text {
margin-top: $unit; margin-top: $unit;
color: $gray-40; color: $gray-40;
font-size: 0.875rem; font-size: $font-size-small;
} }
</style> </style>

View file

@ -1,73 +0,0 @@
<script lang="ts">
interface Props {
label: string
required?: boolean
helpText?: string
error?: string
children?: any
}
let { label, required = false, helpText, error, children }: Props = $props()
</script>
<div class="form-field" class:has-error={!!error}>
<label>
{label}
{#if required}
<span class="required">*</span>
{/if}
</label>
{@render children?.()}
{#if helpText}
<p class="help-text">{helpText}</p>
{/if}
{#if error}
<p class="error-text">{error}</p>
{/if}
</div>
<style lang="scss">
.form-field {
margin-bottom: $unit-3x;
&:last-child {
margin-bottom: 0;
}
&.has-error {
:global(input),
:global(textarea),
:global(select) {
border-color: #c33;
}
}
}
label {
display: block;
margin-bottom: $unit;
font-weight: 500;
color: $gray-20;
font-size: 0.925rem;
.required {
color: #c33;
margin-left: 2px;
}
}
.error-text {
margin-top: $unit;
color: #c33;
font-size: 0.875rem;
}
.help-text {
margin-top: $unit;
color: $gray-40;
font-size: 0.875rem;
}
</style>

View file

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import FormFieldWrapper from './FormFieldWrapper.svelte' import FormField from './FormField.svelte'
interface Option { interface Option {
value: string value: string
@ -31,7 +31,7 @@
} }
</script> </script>
<FormFieldWrapper {label} {required} {helpText} {error}> <FormField {label} {required} {helpText} {error}>
{#snippet children()} {#snippet children()}
<div class="segmented-control-wrapper" class:full-width={fullWidth}> <div class="segmented-control-wrapper" class:full-width={fullWidth}>
<div class="segmented-control"> <div class="segmented-control">
@ -48,7 +48,7 @@
</div> </div>
</div> </div>
{/snippet} {/snippet}
</FormFieldWrapper> </FormField>
<style lang="scss"> <style lang="scss">
.segmented-control-wrapper { .segmented-control-wrapper {

View file

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import Select from './Select.svelte' import Select from './Select.svelte'
import FormFieldWrapper from './FormFieldWrapper.svelte' import FormField from './FormField.svelte'
import type { HTMLSelectAttributes } from 'svelte/elements' import type { HTMLSelectAttributes } from 'svelte/elements'
interface Option { interface Option {
@ -36,11 +36,11 @@
}: Props = $props() }: Props = $props()
</script> </script>
<FormFieldWrapper {label} {required} {helpText} {error}> <FormField {label} {required} {helpText} {error}>
{#snippet children()} {#snippet children()}
<Select bind:value {options} {size} {variant} {fullWidth} {pill} {...restProps} /> <Select bind:value {options} {size} {variant} {fullWidth} {pill} {...restProps} />
{/snippet} {/snippet}
</FormFieldWrapper> </FormField>
<style lang="scss"> <style lang="scss">
// Ensure proper spacing for select fields // Ensure proper spacing for select fields