<script lang="ts" module>export {};
</script>

<script lang="ts" generics="T extends Record<string, unknown>, K extends keyof T & string">import { Utils } from '../../core/utils';
import { default as FieldFrame } from './field-frame.svelte';
let { handler, name, validateOn = ['input', 'change', 'blur'], debounceMs = 0, disableTouchedTracking = false, reserveErrorSpace = false, on, children } = $props();
// True between a value change and the next completed validation. Hides the stale error in the
// UI without mutating handler.errors — the model keeps the last known result, the view just
// doesn't trust it until the validator has caught up.
let pendingValidation = $state(false);
// Monotonic counter: each value change and each validation start bumps it. Completing
// validation clears `pendingValidation` only when the token still matches the latest bump —
// this discards results from validations that were superseded by further input.
let activeToken = 0;
const runValidate = async () => {
    const myToken = ++activeToken;
    try {
        await handler.validateField(name);
    }
    finally {
        // A rejecting validator would otherwise strand pendingValidation, and the children snippet reads it as a never-ending `validating`.
        if (myToken === activeToken) {
            pendingValidation = false;
        }
    }
};
const validate = $derived(debounceMs > 0 ? Utils.debounce(runValidate, debounceMs) : runValidate);
const runs = (mode) => validateOn.includes(mode);
const showErrors = $derived(!pendingValidation && !!handler.errors[name] && (disableTouchedTracking || !!handler.touched[name]));
const field = $derived({
    name,
    value: handler.form[name],
    error: showErrors,
    on: {
        input: (v) => {
            handler.updateField(name, v);
            activeToken++;
            pendingValidation = true;
            if (runs('input')) {
                validate();
            }
            on?.input?.(v);
        },
        change: (v) => {
            handler.updateField(name, v);
            activeToken++;
            pendingValidation = true;
            if (runs('change')) {
                validate();
            }
            on?.change?.(v);
        },
        blur: () => {
            handler.updateTouched(name, true);
            if (runs('blur')) {
                validate();
            }
            else {
                pendingValidation = false;
            }
            on?.blur?.();
        }
    }
});
</script>

<FieldFrame error={showErrors ? handler.errors[name] : null} reserveErrorSpace={reserveErrorSpace}>
  {@render children(field, pendingValidation)}
</FieldFrame>

<!--
@component
Binds a form field to any kit input via a render-prop snippet. The base inputs stay unchanged;
`Validatable` is the only place that knows about `FormValidationHandler`. Wraps children in an
internal `FieldFrame` that reserves space for the error message so toggling doesn't shift
layout; `reserveErrorSpace={false}` drops the reserve — the message overlays content below.

### Usage
```svelte
<Validatable {handler} name="email">
  {#snippet children(field)}
    <Input {...field} placeholder="Email" />
  {/snippet}
</Validatable>
```

The first snippet argument has shape `{ name, value, error, on: { input, change, blur } }` — the
minimal interface every kit input supports. Inputs whose primary value prop is not `value`
(e.g. `Checkbox` uses `checked`) wire the field explicitly inside the snippet.

### Validating flag
The second snippet argument is true from the value change until the validator has caught up — the
debounce window included, which `handler.isValidating` does not cover. Drives an inline async
indicator; everything else it might pair with is already public (`field.error` for the gated error
state, `handler.errors[name]` for the text, `handler.touched[name]`):

```svelte
<Validatable {handler} name="handle" debounceMs={400}>
  {#snippet children(field, validating)}
    <HandleInput {...field} status={validating ? 'checking' : field.error ? 'taken' : handler.touched.handle ? 'available' : undefined} />
  {/snippet}
</Validatable>
```

With a `validateOn` that excludes `input` and `change`, the flag stays true for the whole typing
session — pair an async indicator with the default `validateOn` plus a `debounceMs`.

### Validation events
`validateOn` accepts an array. Default `['input', 'change', 'blur']` validates on every event.
Pass `['blur']` for blur-only validation, `['change', 'blur']` for the classic "validate on commit" pattern.

### Consumer callbacks
Pass extra callbacks via `Validatable`'s own `on={...}` — they run AFTER the validation wiring
and cannot override it.
-->
