diff --git a/packages/core/package.json b/packages/core/package.json index 2993159e3..2ca3c2ea3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -54,7 +54,8 @@ }, "dependencies": { "@types/lodash-es": "^4.17.12", - "axios": "^1.12.2", + "axios": "^1.13.1", + "laravel-precognition": "^0.7.3", "lodash-es": "^4.17.21", "qs": "^6.14.0" }, diff --git a/packages/core/src/resetFormFields.ts b/packages/core/src/resetFormFields.ts index abaecc1a6..f4948506d 100644 --- a/packages/core/src/resetFormFields.ts +++ b/packages/core/src/resetFormFields.ts @@ -161,8 +161,10 @@ export function resetFormFields(formElement: HTMLFormElement, defaults: FormData return } + const resetEntireForm = !fieldNames || fieldNames.length === 0 + // If no specific fields provided, reset the entire form - if (!fieldNames || fieldNames.length === 0) { + if (resetEntireForm) { // Get all field names from both defaults and form elements (including disabled ones) const formData = new FormData(formElement) const formElementNames = Array.from(formElement.elements) @@ -173,7 +175,7 @@ export function resetFormFields(formElement: HTMLFormElement, defaults: FormData let hasChanged = false - fieldNames.forEach((fieldName) => { + fieldNames!.forEach((fieldName) => { const elements = formElement.elements.namedItem(fieldName) if (elements) { @@ -184,7 +186,7 @@ export function resetFormFields(formElement: HTMLFormElement, defaults: FormData }) // Dispatch reset event if any field changed (matching native form.reset() behavior) - if (hasChanged) { + if (hasChanged && resetEntireForm) { formElement.dispatchEvent(new Event('reset', { bubbles: true })) } } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index f8f41b33b..c111cd5c8 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -1,4 +1,5 @@ import { AxiosProgressEvent, AxiosResponse } from 'axios' +import { NamedInputEvent, ValidationConfig, Validator } from 'laravel-precognition' import { Response } from './response' declare module 'axios' { @@ -568,6 +569,9 @@ export type FormComponentProps = Partial< resetOnSuccess?: boolean | string[] resetOnError?: boolean | string[] setDefaultsOnSuccess?: boolean + validateFiles?: boolean + validateTimeout?: number + simpleValidationErrors?: boolean } export type FormComponentMethods = { @@ -580,6 +584,11 @@ export type FormComponentMethods = { defaults: () => void getData: () => Record getFormData: () => FormData + valid: (field: string) => boolean + invalid: (field: string) => boolean + validate(field?: string | NamedInputEvent | ValidationConfig, config?: ValidationConfig): void + touch: (...fields: string[]) => void + touched(field?: string): boolean } export type FormComponentonSubmitCompleteArguments = Pick @@ -592,6 +601,8 @@ export type FormComponentState = { wasSuccessful: boolean recentlySuccessful: boolean isDirty: boolean + validator: Validator + validating: boolean } export type FormComponentSlotProps = FormComponentMethods & FormComponentState diff --git a/packages/react/package.json b/packages/react/package.json index f3fec3a33..1cabd99f6 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -54,7 +54,7 @@ "devDependencies": { "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", - "axios": "^1.12.2", + "axios": "^1.13.1", "es-check": "^9.4.4", "esbuild": "^0.25.11", "esbuild-node-externals": "^1.18.0", @@ -67,6 +67,7 @@ "dependencies": { "@inertiajs/core": "workspace:*", "@types/lodash-es": "^4.17.12", + "laravel-precognition": "^0.7.3", "lodash-es": "^4.17.21" } } diff --git a/packages/react/src/Form.ts b/packages/react/src/Form.ts index 857679be6..1c8da2b2f 100644 --- a/packages/react/src/Form.ts +++ b/packages/react/src/Form.ts @@ -1,4 +1,5 @@ import { + Errors, FormComponentProps, FormComponentRef, FormComponentSlotProps, @@ -10,7 +11,15 @@ import { resetFormFields, VisitOptions, } from '@inertiajs/core' -import { isEqual } from 'lodash-es' +import { + createValidator, + NamedInputEvent, + resolveName, + toSimpleValidationErrors, + ValidationConfig, + Validator, +} from 'laravel-precognition' +import { get, isEqual } from 'lodash-es' import React, { createElement, FormEvent, @@ -64,6 +73,9 @@ const Form = forwardRef( resetOnSuccess = false, setDefaultsOnSuccess = false, invalidateCacheTags = [], + validateFiles = false, + validateTimeout = 1500, + simpleValidationErrors = true, children, ...props }, @@ -79,6 +91,12 @@ const Form = forwardRef( const [isDirty, setIsDirty] = useState(false) const defaultData = useRef(new FormData()) + const [validating, setValidating] = useState(false) + const [valid, setValid] = useState([]) + const [touched, setTouched] = useState([]) + + const [validator, setValidator] = useState() + const getFormData = (): FormData => new FormData(formElement.current) // Convert the FormData to an object because we can't compare two FormData @@ -86,11 +104,37 @@ const Form = forwardRef( // expects an object, and submitting a FormData instance directly causes problems with nested objects. const getData = (): Record => formDataToObject(getFormData()) + const getUrlAndData = (): [string, Record] => { + return mergeDataIntoQueryString( + resolvedMethod, + isUrlMethodPair(action) ? action.url : action, + getData(), + queryStringArrayFormat, + ) + } + const updateDirtyState = (event: Event) => deferStateUpdate(() => setIsDirty(event.type === 'reset' ? false : !isEqual(getData(), formDataToObject(defaultData.current))), ) + const clearErrors = (...names: string[]) => { + form.clearErrors(...names) + + if (names.length === 0) { + validator!.setErrors({}) + } else { + names.forEach(validator!.forgetError) + } + + return form + } + + const getTransformedData = (): Record => { + const [_url, data] = getUrlAndData() + return transform(data) + } + useEffect(() => { defaultData.current = getFormData() @@ -98,17 +142,77 @@ const Form = forwardRef( formEvents.forEach((e) => formElement.current!.addEventListener(e, updateDirtyState)) - return () => formEvents.forEach((e) => formElement.current?.removeEventListener(e, updateDirtyState)) + // Initialize validator + const newValidator = createValidator( + (client) => + client[resolvedMethod](getUrlAndData()[0], getTransformedData(), { + headers, + }), + getTransformedData(), + ) + .on('validatingChanged', () => { + setValidating(newValidator.validating()) + }) + .on('validatedChanged', () => { + setValid(newValidator.valid()) + }) + .on('touchedChanged', () => { + setTouched(newValidator.touched()) + }) + .on('errorsChanged', () => { + form.clearErrors() + + const errors = simpleValidationErrors + ? toSimpleValidationErrors(newValidator.errors()) + : newValidator.errors() + + form.setError(errors as Errors) + + setValid(newValidator.valid()) + }) + + newValidator.setTimeout(validateTimeout) + + if (validateFiles) { + newValidator.validateFiles() + } + + setValidator(newValidator) + + return () => { + formEvents.forEach((e) => formElement.current?.removeEventListener(e, updateDirtyState)) + } }, []) + const validate = (field?: string | NamedInputEvent | ValidationConfig, config?: ValidationConfig) => { + if (typeof field === 'object' && !('target' in field)) { + config = field + field = undefined + } + + if (typeof field === 'undefined') { + validator!.validate(config) + } else { + field = resolveName(field) + + validator!.validate(field, get(getTransformedData(), field), config) + } + } + + useEffect(() => { + validator?.setTimeout(validateTimeout) + }, [validateTimeout, validator]) + const reset = (...fields: string[]) => { if (formElement.current) { resetFormFields(formElement.current, defaultData.current, fields) } + + validator!.reset(...fields) } const resetAndClearErrors = (...fields: string[]) => { - form.clearErrors(...fields) + clearErrors(...fields) reset(...fields) } @@ -125,12 +229,7 @@ const Form = forwardRef( } const submit = () => { - const [url, _data] = mergeDataIntoQueryString( - resolvedMethod, - isUrlMethodPair(action) ? action.url : action, - getData(), - queryStringArrayFormat, - ) + const [url, _data] = getUrlAndData() const submitOptions: FormSubmitOptions = { headers, @@ -171,6 +270,18 @@ const Form = forwardRef( setIsDirty(false) } + const touch = (...fields: string[]) => { + validator!.touch(fields) + } + + const isTouched = (field?: string): boolean => { + if (typeof field === 'string') { + return touched.includes(field) + } + + return touched.length > 0 + } + const exposed = () => ({ errors: form.errors, hasErrors: form.hasErrors, @@ -179,7 +290,7 @@ const Form = forwardRef( wasSuccessful: form.wasSuccessful, recentlySuccessful: form.recentlySuccessful, isDirty, - clearErrors: form.clearErrors, + clearErrors, resetAndClearErrors, setError: form.setError, reset, @@ -187,9 +298,18 @@ const Form = forwardRef( defaults, getData, getFormData, + + // Precognition + validator: validator!, + validating, + valid: (field: string) => valid.includes(field), + invalid: (field: string) => form.errors[field] !== undefined, + validate, + touch, + touched: isTouched, }) - useImperativeHandle(ref, exposed, [form, isDirty, submit]) + useImperativeHandle(ref, exposed, [form, isDirty, submit, validating, valid, touched, touch, validator]) return createElement( 'form', diff --git a/packages/react/test-app/Pages/FormComponent/Precognition.tsx b/packages/react/test-app/Pages/FormComponent/Precognition.tsx new file mode 100644 index 000000000..2ff56745b --- /dev/null +++ b/packages/react/test-app/Pages/FormComponent/Precognition.tsx @@ -0,0 +1,29 @@ +import { Form } from '@inertiajs/react' + +export default () => { + return ( +
+

Form Precognition

+ +
+ {({ invalid, errors, validate, valid, validating }) => ( + <> +
+ validate('name')} /> + {invalid('name') &&

{errors.name}

} + {valid('name') &&

Name is valid!

} +
+ +
+ validate('email')} /> + {invalid('email') &&

{errors.email}

} + {valid('email') &&

Email is valid!

} +
+ + {validating &&

Validating...

} + + )} +
+
+ ) +} diff --git a/packages/react/test-app/Pages/FormComponent/PrecognitionAllErrors.tsx b/packages/react/test-app/Pages/FormComponent/PrecognitionAllErrors.tsx new file mode 100644 index 000000000..4b63a3df7 --- /dev/null +++ b/packages/react/test-app/Pages/FormComponent/PrecognitionAllErrors.tsx @@ -0,0 +1,58 @@ +import { Form } from '@inertiajs/react' + +export default () => { + return ( +
+

Form Precognition - All Errors

+ +
+ {({ invalid, errors, validate, valid, validating }) => ( + <> +
+ validate('name')} /> + {invalid('name') && ( +
+ {Array.isArray(errors.name) ? ( + errors.name.map((error, index) => ( +

+ {error} +

+ )) + ) : ( +

{errors.name}

+ )} +
+ )} + {valid('name') &&

Name is valid!

} +
+ +
+ validate('email')} /> + {invalid('email') && ( +
+ {Array.isArray(errors.email) ? ( + errors.email.map((error, index) => ( +

+ {error} +

+ )) + ) : ( +

{errors.email}

+ )} +
+ )} + {valid('email') &&

Email is valid!

} +
+ + {validating &&

Validating...

} + + )} +
+
+ ) +} diff --git a/packages/react/test-app/Pages/FormComponent/PrecognitionArrayErrors.tsx b/packages/react/test-app/Pages/FormComponent/PrecognitionArrayErrors.tsx new file mode 100644 index 000000000..a1a4ffe79 --- /dev/null +++ b/packages/react/test-app/Pages/FormComponent/PrecognitionArrayErrors.tsx @@ -0,0 +1,29 @@ +import { Form } from '@inertiajs/react' + +export default () => { + return ( +
+

Form Precognition - Array Errors

+ +
+ {({ invalid, errors, validate, valid, validating }) => ( + <> +
+ validate('name')} /> + {invalid('name') &&

{errors.name}

} + {valid('name') &&

Name is valid!

} +
+ +
+ validate('email')} /> + {invalid('email') &&

{errors.email}

} + {valid('email') &&

Email is valid!

} +
+ + {validating &&

Validating...

} + + )} +
+
+ ) +} diff --git a/packages/react/test-app/Pages/FormComponent/PrecognitionBeforeValidation.tsx b/packages/react/test-app/Pages/FormComponent/PrecognitionBeforeValidation.tsx new file mode 100644 index 000000000..3e8149d55 --- /dev/null +++ b/packages/react/test-app/Pages/FormComponent/PrecognitionBeforeValidation.tsx @@ -0,0 +1,55 @@ +import { Form } from '@inertiajs/react' +import { isEqual } from 'lodash-es' + +export default function PrecognitionBefore() { + const handleBeforeValidation = ( + newRequest: { data: Record | null; touched: string[] }, + oldRequest: { data: Record | null; touched: string[] }, + ) => { + const payloadIsCorrect = + isEqual(newRequest, { data: { name: 'block' }, touched: ['name'] }) && + isEqual(oldRequest, { data: {}, touched: [] }) + + if (payloadIsCorrect && newRequest.data?.name === 'block') { + return false + } + + return true + } + + return ( +
+

Precognition - onBefore

+ +
+ {({ errors, invalid, validate, validating }) => ( + <> +
+ + + validate('name', { + onBeforeValidation: handleBeforeValidation, + }) + } + /> + {invalid('name') &&

{errors.name}

} +
+ +
+ + validate('email')} /> + {invalid('email') &&

{errors.email}

} +
+ + {validating &&

Validating...

} + + + + )} +
+
+ ) +} diff --git a/packages/react/test-app/Pages/FormComponent/PrecognitionCallbacks.tsx b/packages/react/test-app/Pages/FormComponent/PrecognitionCallbacks.tsx new file mode 100644 index 000000000..47735527b --- /dev/null +++ b/packages/react/test-app/Pages/FormComponent/PrecognitionCallbacks.tsx @@ -0,0 +1,52 @@ +import { Form } from '@inertiajs/react' +import { useState } from 'react' + +export default () => { + const [successCalled, setSuccessCalled] = useState(false) + const [errorCalled, setErrorCalled] = useState(false) + const [finishCalled, setFinishCalled] = useState(false) + + return ( +
+

Form Precognition Callbacks

+ +

Callbacks Test

+
+ {({ validate, validating, touch }) => ( + <> +
+ touch('name')} /> +
+ + {validating &&

Validating...

} + {successCalled &&

onPrecognitionSuccess called!

} + {errorCalled &&

onValidationError called!

} + {finishCalled &&

onFinish called!

} + + + + )} +
+
+ ) +} diff --git a/packages/react/test-app/Pages/FormComponent/PrecognitionCancel.tsx b/packages/react/test-app/Pages/FormComponent/PrecognitionCancel.tsx new file mode 100644 index 000000000..5fa49b494 --- /dev/null +++ b/packages/react/test-app/Pages/FormComponent/PrecognitionCancel.tsx @@ -0,0 +1,25 @@ +import { Form } from '@inertiajs/react' + +export default () => { + return ( +
+

Precognition - Cancel Tests

+ +

Auto Cancel Test

+
+ {({ invalid, errors, validate, validating }) => ( + <> +
+ validate('name')} /> + {invalid('name') &&

{errors.name}

} +
+ + {validating &&

Validating...

} + + + + )} +
+
+ ) +} diff --git a/packages/react/test-app/Pages/FormComponent/PrecognitionFiles.tsx b/packages/react/test-app/Pages/FormComponent/PrecognitionFiles.tsx new file mode 100644 index 000000000..c438494e8 --- /dev/null +++ b/packages/react/test-app/Pages/FormComponent/PrecognitionFiles.tsx @@ -0,0 +1,45 @@ +import { Form } from '@inertiajs/react' +import { useState } from 'react' + +export default () => { + const [validateFilesEnabled, setValidateFilesEnabled] = useState(false) + + return ( +
+

Form Precognition Files

+ +
+ {({ invalid, errors, validate, valid, validating }) => ( + <> +
+ validate('name')} /> + {invalid('name') &&

{errors.name}

} + {valid('name') &&

Name is valid!

} +
+ +
+ + {invalid('avatar') &&

{errors.avatar}

} + {valid('avatar') &&

Avatar is valid!

} +
+ + {validating &&

Validating...

} + + + + + + )} +
+
+ ) +} diff --git a/packages/react/test-app/Pages/FormComponent/PrecognitionHeaders.tsx b/packages/react/test-app/Pages/FormComponent/PrecognitionHeaders.tsx new file mode 100644 index 000000000..edbd99d9f --- /dev/null +++ b/packages/react/test-app/Pages/FormComponent/PrecognitionHeaders.tsx @@ -0,0 +1,29 @@ +import { Form } from '@inertiajs/react' + +export default function PrecognitionHeaders() { + return ( +
+

Precognition - Custom Headers

+ +
+ {({ invalid, errors, validate, validating }) => ( + <> +
+ validate('name')} /> + {invalid('name') &&

{errors.name}

} +
+ + {validating &&

Validating...

} + + + + )} +
+
+ ) +} diff --git a/packages/react/test-app/Pages/FormComponent/PrecognitionMethods.tsx b/packages/react/test-app/Pages/FormComponent/PrecognitionMethods.tsx new file mode 100644 index 000000000..b29f5d8d4 --- /dev/null +++ b/packages/react/test-app/Pages/FormComponent/PrecognitionMethods.tsx @@ -0,0 +1,62 @@ +import { Form } from '@inertiajs/react' + +export default () => { + return ( +
+

Form Precognition - Touch, Reset & Validate

+ +
+ {({ invalid, errors, validate, touch, touched, validating, reset }) => ( + <> +
+ touch('name')} /> + {invalid('name') &&

{errors.name}

} +
+ +
+ touch('email')} /> + {invalid('email') &&

{errors.email}

} +
+ + {validating &&

Validating...

} + +

{touched('name') ? 'Name is touched' : 'Name is not touched'}

+

{touched('email') ? 'Email is touched' : 'Email is not touched'}

+

{touched() ? 'Form has touched fields' : 'Form has no touched fields'}

+ + + + + + + + + + + )} +
+
+ ) +} diff --git a/packages/react/test-app/Pages/FormComponent/PrecognitionTransform.tsx b/packages/react/test-app/Pages/FormComponent/PrecognitionTransform.tsx new file mode 100644 index 000000000..ced7a4552 --- /dev/null +++ b/packages/react/test-app/Pages/FormComponent/PrecognitionTransform.tsx @@ -0,0 +1,28 @@ +import { Form } from '@inertiajs/react' + +export default () => { + return ( +
+

Form Precognition Transform

+ +
({ name: String(data.name || '').repeat(2) })} + > + {({ invalid, errors, validate, valid, validating }) => ( + <> +
+ validate('name')} /> + {invalid('name') &&

{errors.name}

} + {valid('name') &&

Name is valid!

} +
+ + {validating &&

Validating...

} + + )} +
+
+ ) +} diff --git a/packages/react/test-app/package.json b/packages/react/test-app/package.json index 383696be6..332e735f0 100755 --- a/packages/react/test-app/package.json +++ b/packages/react/test-app/package.json @@ -8,7 +8,7 @@ }, "devDependencies": { "@eslint/js": "^9.38.0", - "@types/node": "^24.9.1", + "@types/node": "^24.9.2", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "eslint": "^9.38.0", diff --git a/packages/svelte/package.json b/packages/svelte/package.json index afbc0adea..f36398356 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -45,10 +45,10 @@ }, "devDependencies": { "@sveltejs/adapter-auto": "^3.3.1", - "@sveltejs/kit": "^2.47.2", + "@sveltejs/kit": "^2.48.3", "@sveltejs/package": "^2.5.4", "@sveltejs/vite-plugin-svelte": "^3.1.2", - "axios": "^1.12.2", + "axios": "^1.13.1", "es-check": "^9.4.4", "publint": "^0.2.12", "svelte": "^4.2.20", @@ -63,6 +63,7 @@ "dependencies": { "@inertiajs/core": "workspace:*", "@types/lodash-es": "^4.17.12", + "laravel-precognition": "^0.7.3", "lodash-es": "^4.17.21" } } diff --git a/packages/svelte/src/components/Form.svelte b/packages/svelte/src/components/Form.svelte index d9820967d..5fefd45d3 100644 --- a/packages/svelte/src/components/Form.svelte +++ b/packages/svelte/src/components/Form.svelte @@ -10,7 +10,15 @@ type VisitOptions, isUrlMethodPair, } from '@inertiajs/core' - import { isEqual } from 'lodash-es' + import { + createValidator, + resolveName, + toSimpleValidationErrors, + type NamedInputEvent, + type ValidationConfig, + type Validator, + } from 'laravel-precognition' + import { get, isEqual } from 'lodash-es' import { onMount } from 'svelte' import useForm from '../useForm' @@ -38,6 +46,9 @@ export let resetOnError: FormComponentProps['resetOnError'] = false export let resetOnSuccess: FormComponentProps['resetOnSuccess'] = false export let setDefaultsOnSuccess: FormComponentProps['setDefaultsOnSuccess'] = false + export let validateFiles: FormComponentProps['validateFiles'] = false + export let validateTimeout: FormComponentProps['validateTimeout'] = 1500 + export let simpleValidationErrors: FormComponentProps['simpleValidationErrors'] = true type FormSubmitOptions = Omit @@ -46,6 +57,11 @@ let isDirty = false let defaultData: FormData = new FormData() + let validating = false + let validFields: string[] = [] + let touchedFields: string[] = [] + let validator: Validator + $: _method = isUrlMethodPair(action) ? action.method : ((method ?? 'get').toLowerCase() as Method) $: _action = isUrlMethodPair(action) ? action.url : (action as string) @@ -60,12 +76,21 @@ return formDataToObject(getFormData()) } + function getUrlAndData(): [string, Record] { + return mergeDataIntoQueryString(_method, _action, getData(), queryStringArrayFormat) + } + function updateDirtyState(event: Event) { isDirty = event.type === 'reset' ? false : !isEqual(getData(), formDataToObject(defaultData)) } + function getTransformedData(): Record { + const [_url, data] = getUrlAndData() + return transform!(data) + } + export function submit() { - const [url, _data] = mergeDataIntoQueryString(_method, _action, getData(), queryStringArrayFormat) + const [url, _data] = getUrlAndData() const maybeReset = (resetOption: boolean | string[] | undefined) => { if (!resetOption) { @@ -136,16 +161,23 @@ export function reset(...fields: string[]) { resetFormFields(formElement, defaultData, fields) + + validator!.reset(...fields) } export function clearErrors(...fields: string[]) { // @ts-expect-error $form.clearErrors(...fields) + + if (fields.length === 0) { + validator!.setErrors({}) + } else { + fields.forEach(validator!.forgetError) + } } export function resetAndClearErrors(...fields: string[]) { - // @ts-expect-error - $form.clearErrors(...fields) + clearErrors(...fields) reset(...fields) } @@ -163,17 +195,96 @@ isDirty = false } + export function validate(field?: string | NamedInputEvent | ValidationConfig, config?: ValidationConfig) { + if (typeof field === 'object' && !('target' in field)) { + config = field + field = undefined + } + + if (typeof field === 'undefined') { + validator!.validate(config) + } else { + field = resolveName(field) + + validator!.validate(field, get(getTransformedData(), field), config) + } + } + + export function touch(...fields: string[]) { + validator!.touch(fields) + } + + export function touched(field?: string): boolean { + if (typeof field === 'string') { + return touchedFields.includes(field) + } + + return touchedFields.length > 0 + } + + export function valid(field: string): boolean { + return validFields.includes(field) + } + + export function invalid(field: string): boolean { + // @ts-expect-error + return $form.errors[field] !== undefined + } + onMount(() => { defaultData = getFormData() const formEvents = ['input', 'change', 'reset'] formEvents.forEach((e) => formElement.addEventListener(e, updateDirtyState)) + // Initialize validator + validator = createValidator( + (client) => + client[_method!](getUrlAndData()[0], getTransformedData(), { + headers, + }), + getTransformedData(), + ) + .on('validatingChanged', () => { + validating = validator!.validating() + }) + .on('validatedChanged', () => { + validFields = validator!.valid() + }) + .on('touchedChanged', () => { + touchedFields = validator!.touched() + }) + .on('errorsChanged', () => { + $form.clearErrors() + + const errors = simpleValidationErrors ? toSimpleValidationErrors(validator!.errors()) : validator!.errors() + + $form.setError(errors as Errors) + validFields = validator!.valid() + }) + + validator.setTimeout(validateTimeout!) + + if (validateFiles) { + validator.validateFiles() + } + return () => { formEvents.forEach((e) => formElement?.removeEventListener(e, updateDirtyState)) } }) + + $: { + validator?.setTimeout(validateTimeout!) + } + $: slotErrors = $form.errors as Errors + + // Create reactive slot props that update when state changes + $: validMethod = (field: string) => validFields.includes(field) + $: invalidMethod = (field: string) => slotErrors[field] !== undefined + $: touchedMethod = (field?: string) => + typeof field === 'string' ? touchedFields.includes(field) : touchedFields.length > 0
diff --git a/packages/svelte/test-app/Pages/FormComponent/Precognition.svelte b/packages/svelte/test-app/Pages/FormComponent/Precognition.svelte new file mode 100644 index 000000000..42a41c237 --- /dev/null +++ b/packages/svelte/test-app/Pages/FormComponent/Precognition.svelte @@ -0,0 +1,42 @@ + + +
+

Form Precognition

+ +
+
+ validate('name')} /> + {#if invalid('name')} +

{errors.name}

+ {/if} + {#if valid('name')} +

Name is valid!

+ {/if} +
+ +
+ validate('email')} /> + {#if invalid('email')} +

{errors.email}

+ {/if} + {#if valid('email')} +

Email is valid!

+ {/if} +
+ + {#if validating} +

Validating...

+ {/if} +
+
diff --git a/packages/svelte/test-app/Pages/FormComponent/PrecognitionAllErrors.svelte b/packages/svelte/test-app/Pages/FormComponent/PrecognitionAllErrors.svelte new file mode 100644 index 000000000..f935507e4 --- /dev/null +++ b/packages/svelte/test-app/Pages/FormComponent/PrecognitionAllErrors.svelte @@ -0,0 +1,59 @@ + + +
+

Form Precognition - All Errors

+ +
+
+ validate('name')} /> + {#if invalid('name')} +
+ {#if Array.isArray(errors.name)} + {#each errors.name as error, index (index)} +

{error}

+ {/each} + {:else} +

{errors.name}

+ {/if} +
+ {/if} + {#if valid('name')} +

Name is valid!

+ {/if} +
+ +
+ validate('email')} /> + {#if invalid('email')} +
+ {#if Array.isArray(errors.email)} + {#each errors.email as error, index (index)} +

{error}

+ {/each} + {:else} +

{errors.email}

+ {/if} +
+ {/if} + {#if valid('email')} +

Email is valid!

+ {/if} +
+ + {#if validating} +

Validating...

+ {/if} +
+
diff --git a/packages/svelte/test-app/Pages/FormComponent/PrecognitionArrayErrors.svelte b/packages/svelte/test-app/Pages/FormComponent/PrecognitionArrayErrors.svelte new file mode 100644 index 000000000..252e533ca --- /dev/null +++ b/packages/svelte/test-app/Pages/FormComponent/PrecognitionArrayErrors.svelte @@ -0,0 +1,42 @@ + + +
+

Form Precognition - Array Errors

+ +
+
+ validate('name')} /> + {#if invalid('name')} +

{errors.name}

+ {/if} + {#if valid('name')} +

Name is valid!

+ {/if} +
+ +
+ validate('email')} /> + {#if invalid('email')} +

{errors.email}

+ {/if} + {#if valid('email')} +

Email is valid!

+ {/if} +
+ + {#if validating} +

Validating...

+ {/if} +
+
diff --git a/packages/svelte/test-app/Pages/FormComponent/PrecognitionBeforeValidation.svelte b/packages/svelte/test-app/Pages/FormComponent/PrecognitionBeforeValidation.svelte new file mode 100644 index 000000000..82b01f9b5 --- /dev/null +++ b/packages/svelte/test-app/Pages/FormComponent/PrecognitionBeforeValidation.svelte @@ -0,0 +1,63 @@ + + +
+

Precognition - onBefore

+ +
+
+ + + validate('name', { + onBeforeValidation: handleBeforeValidation, + })} + /> + {#if invalid('name')} +

{errors.name}

+ {/if} +
+ +
+ + validate('email')} /> + {#if invalid('email')} +

{errors.email}

+ {/if} +
+ + {#if validating} +

Validating...

+ {/if} + + +
+
diff --git a/packages/svelte/test-app/Pages/FormComponent/PrecognitionCallbacks.svelte b/packages/svelte/test-app/Pages/FormComponent/PrecognitionCallbacks.svelte new file mode 100644 index 000000000..1b85ddba3 --- /dev/null +++ b/packages/svelte/test-app/Pages/FormComponent/PrecognitionCallbacks.svelte @@ -0,0 +1,53 @@ + + +
+

Form Precognition Callbacks

+ +

Callbacks Test

+
+
+ touch('name')} /> +
+ + {#if validating} +

Validating...

+ {/if} + {#if successCalled} +

onPrecognitionSuccess called!

+ {/if} + {#if errorCalled} +

onValidationError called!

+ {/if} + {#if finishCalled} +

onFinish called!

+ {/if} + + +
+
diff --git a/packages/svelte/test-app/Pages/FormComponent/PrecognitionCancel.svelte b/packages/svelte/test-app/Pages/FormComponent/PrecognitionCancel.svelte new file mode 100644 index 000000000..b670c1fe7 --- /dev/null +++ b/packages/svelte/test-app/Pages/FormComponent/PrecognitionCancel.svelte @@ -0,0 +1,33 @@ + + +
+

Precognition - Cancel Tests

+ +

Auto Cancel Test

+
+
+ validate('name')} /> + {#if invalid('name')} +

+ {errors.name} +

+ {/if} +
+ + {#if validating} +

Validating...

+ {/if} + + +
+
diff --git a/packages/svelte/test-app/Pages/FormComponent/PrecognitionFiles.svelte b/packages/svelte/test-app/Pages/FormComponent/PrecognitionFiles.svelte new file mode 100644 index 000000000..94659c2a5 --- /dev/null +++ b/packages/svelte/test-app/Pages/FormComponent/PrecognitionFiles.svelte @@ -0,0 +1,51 @@ + + +
+

Form Precognition Files

+ +
+
+ validate('name')} /> + {#if invalid('name')} +

{errors.name}

+ {/if} + {#if valid('name')} +

Name is valid!

+ {/if} +
+ +
+ + {#if invalid('avatar')} +

{errors.avatar}

+ {/if} + {#if valid('avatar')} +

Avatar is valid!

+ {/if} +
+ + {#if validating} +

Validating...

+ {/if} + + + + +
+
diff --git a/packages/svelte/test-app/Pages/FormComponent/PrecognitionHeaders.svelte b/packages/svelte/test-app/Pages/FormComponent/PrecognitionHeaders.svelte new file mode 100644 index 000000000..ada432b86 --- /dev/null +++ b/packages/svelte/test-app/Pages/FormComponent/PrecognitionHeaders.svelte @@ -0,0 +1,33 @@ + + +
+

Precognition - Custom Headers

+ +
+
+ validate('name')} /> + {#if invalid('name')} +

+ {errors.name} +

+ {/if} +
+ + {#if validating} +

Validating...

+ {/if} + + +
+
diff --git a/packages/svelte/test-app/Pages/FormComponent/PrecognitionMethods.svelte b/packages/svelte/test-app/Pages/FormComponent/PrecognitionMethods.svelte new file mode 100644 index 000000000..65fa58ef6 --- /dev/null +++ b/packages/svelte/test-app/Pages/FormComponent/PrecognitionMethods.svelte @@ -0,0 +1,59 @@ + + +
+

Form Precognition - Touch, Reset & Validate

+ +
+
+ touch('name')} /> + {#if invalid('name')} +

{errors.name}

+ {/if} +
+ +
+ touch('email')} /> + {#if invalid('email')} +

{errors.email}

+ {/if} +
+ + {#if validating} +

Validating...

+ {/if} + +

{touched('name') ? 'Name is touched' : 'Name is not touched'}

+

{touched('email') ? 'Email is touched' : 'Email is not touched'}

+

{touched() ? 'Form has touched fields' : 'Form has no touched fields'}

+ + + + + + + + + +
+
diff --git a/packages/svelte/test-app/Pages/FormComponent/PrecognitionTransform.svelte b/packages/svelte/test-app/Pages/FormComponent/PrecognitionTransform.svelte new file mode 100644 index 000000000..a4f2f0785 --- /dev/null +++ b/packages/svelte/test-app/Pages/FormComponent/PrecognitionTransform.svelte @@ -0,0 +1,33 @@ + + +
+

Form Precognition Transform

+ +
({ name: String(data.name || '').repeat(2) })} + let:invalid + let:errors + let:validate + let:valid + let:validating + > +
+ validate('name')} /> + {#if invalid('name')} +

{errors.name}

+ {/if} + {#if valid('name')} +

Name is valid!

+ {/if} +
+ + {#if validating} +

Validating...

+ {/if} +
+
diff --git a/packages/svelte/test-app/package.json b/packages/svelte/test-app/package.json index 1c20d49a0..bcaf97e03 100644 --- a/packages/svelte/test-app/package.json +++ b/packages/svelte/test-app/package.json @@ -12,7 +12,7 @@ "@sveltejs/vite-plugin-svelte": "^3.1.2", "@tsconfig/svelte": "^5.0.5", "eslint": "^9.38.0", - "eslint-plugin-svelte": "^3.12.5", + "eslint-plugin-svelte": "^3.13.0", "globals": "^16.4.0", "nodemon": "^3.1.10", "svelte": "^4.2.20", diff --git a/packages/vue3/package.json b/packages/vue3/package.json index fea8479b9..685ce06a4 100644 --- a/packages/vue3/package.json +++ b/packages/vue3/package.json @@ -51,8 +51,8 @@ "es2020-check": "pnpm build:with-deps && es-check es2020 \"dist/index.esm.js\" --checkFeatures --module --noCache --verbose" }, "devDependencies": { - "@types/node": "^22.18.12", - "axios": "^1.12.2", + "@types/node": "^22.18.13", + "axios": "^1.13.1", "es-check": "^9.4.4", "esbuild": "^0.25.11", "esbuild-node-externals": "^1.18.0", @@ -65,6 +65,7 @@ "dependencies": { "@inertiajs/core": "workspace:*", "@types/lodash-es": "^4.17.12", + "laravel-precognition": "^0.7.3", "lodash-es": "^4.17.21" } } diff --git a/packages/vue3/src/form.ts b/packages/vue3/src/form.ts index 35f18e6e9..789950534 100644 --- a/packages/vue3/src/form.ts +++ b/packages/vue3/src/form.ts @@ -11,8 +11,16 @@ import { resetFormFields, VisitOptions, } from '@inertiajs/core' -import { isEqual } from 'lodash-es' -import { computed, defineComponent, h, onBeforeUnmount, onMounted, PropType, ref, SlotsType } from 'vue' +import { + createValidator, + NamedInputEvent, + resolveName, + toSimpleValidationErrors, + ValidationConfig, + Validator, +} from 'laravel-precognition' +import { get, isEqual } from 'lodash-es' +import { computed, defineComponent, h, onBeforeUnmount, onMounted, PropType, ref, SlotsType, watch } from 'vue' import useForm from './useForm' type FormSubmitOptions = Omit @@ -113,6 +121,18 @@ const Form = defineComponent({ type: [String, Array] as PropType, default: () => [], }, + validateFiles: { + type: Boolean as PropType, + default: false, + }, + validateTimeout: { + type: Number as PropType, + default: 1500, + }, + simpleValidationErrors: { + type: Boolean as PropType, + default: true, + }, }, setup(props, { slots, attrs, expose }) { const form = useForm>({}) @@ -135,11 +155,94 @@ const Form = defineComponent({ const formEvents: Array = ['input', 'change', 'reset'] + const validating = ref(false) + const valid = ref([]) + const touched = ref([]) + + let validator: Validator + + const clearErrors = (...names: string[]) => { + form.clearErrors(...names) + + if (names.length === 0) { + validator.setErrors({}) + } else { + names.forEach(validator.forgetError) + } + + return form + } + + const getTransformedData = (): Record => { + const [_url, data] = getUrlAndData() + + return props.transform(data) + } + onMounted(() => { + validator = createValidator( + (client) => + client[method.value](getUrlAndData()[0], getTransformedData(), { + headers: props.headers, + }), + getTransformedData(), + ) + .on('validatingChanged', () => { + validating.value = validator.validating() + }) + .on('validatedChanged', () => { + valid.value = validator.valid() + }) + .on('touchedChanged', () => { + touched.value = validator.touched() + }) + .on('errorsChanged', () => { + form.clearErrors() + + const errors = props.simpleValidationErrors + ? toSimpleValidationErrors(validator.errors()) + : validator.errors() + + form.setError(errors as Errors) + + valid.value = validator.valid() + }) + + validator.setTimeout(props.validateTimeout) + + if (props.validateFiles) { + validator.validateFiles() + } + defaultData.value = getFormData() formEvents.forEach((e) => formElement.value.addEventListener(e, onFormUpdate)) }) + const validate = (field?: string | NamedInputEvent | ValidationConfig, config?: ValidationConfig) => { + if (typeof field === 'object' && !('target' in field)) { + config = field + field = undefined + } + + if (typeof field === 'undefined') { + validator.validate(config) + } else { + field = resolveName(field) + + validator.validate(field, get(getTransformedData(), field), config) + } + } + + // watch( + // () => props.validateFiles, + // (value) => validator.validateFiles(value), + // ) + + watch( + () => props.validateTimeout, + (value) => validator.setTimeout(value), + ) + onBeforeUnmount(() => formEvents.forEach((e) => formElement.value?.removeEventListener(e, onFormUpdate))) const getFormData = (): FormData => new FormData(formElement.value) @@ -149,14 +252,16 @@ const Form = defineComponent({ // expects an object, and submitting a FormData instance directly causes problems with nested objects. const getData = (): Record => formDataToObject(getFormData()) - const submit = () => { - const [action, data] = mergeDataIntoQueryString( + const getUrlAndData = (): [string, Record] => { + return mergeDataIntoQueryString( method.value, isUrlMethodPair(props.action) ? props.action.url : props.action, getData(), props.queryStringArrayFormat, ) + } + const submit = () => { const maybeReset = (resetOption: boolean | string[]) => { if (!resetOption) { return @@ -196,16 +301,20 @@ const Form = defineComponent({ ...props.options, } + const [url, data] = getUrlAndData() + // We need transform because we can't override the default data with different keys (by design) - form.transform(() => props.transform(data)).submit(method.value, action, submitOptions) + form.transform(() => props.transform(data)).submit(method.value, url, submitOptions) } const reset = (...fields: string[]) => { resetFormFields(formElement.value, defaultData.value, fields) + + validator.reset(...fields) } const resetAndClearErrors = (...fields: string[]) => { - form.clearErrors(...fields) + clearErrors(...fields) reset(...fields) } @@ -214,6 +323,14 @@ const Form = defineComponent({ isDirty.value = false } + const isTouched = (field?: string): boolean => { + if (typeof field === 'string') { + return touched.value.includes(field) + } + + return touched.value.length > 0 + } + const exposed = { get errors() { return form.errors @@ -233,7 +350,13 @@ const Form = defineComponent({ get recentlySuccessful() { return form.recentlySuccessful }, - clearErrors: (...fields: string[]) => form.clearErrors(...fields), + get validator() { + return validator + }, + get validating() { + return validating.value + }, + clearErrors, resetAndClearErrors, setError: (fieldOrFields: string | Record, maybeValue?: string) => form.setError((typeof fieldOrFields === 'string' ? { [fieldOrFields]: maybeValue } : fieldOrFields) as Errors), @@ -245,6 +368,13 @@ const Form = defineComponent({ defaults, getData, getFormData, + + // Precognition + valid: (field: string) => valid.value.includes(field), + invalid: (field: string) => form.errors[field] !== undefined, + validate, + touch: (...fields: string[]) => validator.touch(fields), + touched: isTouched, } expose(exposed) diff --git a/packages/vue3/test-app/Pages/FormComponent/Precognition.vue b/packages/vue3/test-app/Pages/FormComponent/Precognition.vue new file mode 100644 index 000000000..ce7ae4213 --- /dev/null +++ b/packages/vue3/test-app/Pages/FormComponent/Precognition.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/vue3/test-app/Pages/FormComponent/PrecognitionAllErrors.vue b/packages/vue3/test-app/Pages/FormComponent/PrecognitionAllErrors.vue new file mode 100644 index 000000000..9e4519de0 --- /dev/null +++ b/packages/vue3/test-app/Pages/FormComponent/PrecognitionAllErrors.vue @@ -0,0 +1,45 @@ + + + diff --git a/packages/vue3/test-app/Pages/FormComponent/PrecognitionArrayErrors.vue b/packages/vue3/test-app/Pages/FormComponent/PrecognitionArrayErrors.vue new file mode 100644 index 000000000..559d650a2 --- /dev/null +++ b/packages/vue3/test-app/Pages/FormComponent/PrecognitionArrayErrors.vue @@ -0,0 +1,34 @@ + + + diff --git a/packages/vue3/test-app/Pages/FormComponent/PrecognitionBeforeValidation.vue b/packages/vue3/test-app/Pages/FormComponent/PrecognitionBeforeValidation.vue new file mode 100644 index 000000000..3ed761748 --- /dev/null +++ b/packages/vue3/test-app/Pages/FormComponent/PrecognitionBeforeValidation.vue @@ -0,0 +1,56 @@ + + + diff --git a/packages/vue3/test-app/Pages/FormComponent/PrecognitionCallbacks.vue b/packages/vue3/test-app/Pages/FormComponent/PrecognitionCallbacks.vue new file mode 100644 index 000000000..824d98b4b --- /dev/null +++ b/packages/vue3/test-app/Pages/FormComponent/PrecognitionCallbacks.vue @@ -0,0 +1,52 @@ + + + diff --git a/packages/vue3/test-app/Pages/FormComponent/PrecognitionCancel.vue b/packages/vue3/test-app/Pages/FormComponent/PrecognitionCancel.vue new file mode 100644 index 000000000..bfb683e60 --- /dev/null +++ b/packages/vue3/test-app/Pages/FormComponent/PrecognitionCancel.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/vue3/test-app/Pages/FormComponent/PrecognitionFiles.vue b/packages/vue3/test-app/Pages/FormComponent/PrecognitionFiles.vue new file mode 100644 index 000000000..8e55b5374 --- /dev/null +++ b/packages/vue3/test-app/Pages/FormComponent/PrecognitionFiles.vue @@ -0,0 +1,44 @@ + + + diff --git a/packages/vue3/test-app/Pages/FormComponent/PrecognitionHeaders.vue b/packages/vue3/test-app/Pages/FormComponent/PrecognitionHeaders.vue new file mode 100644 index 000000000..b006055e7 --- /dev/null +++ b/packages/vue3/test-app/Pages/FormComponent/PrecognitionHeaders.vue @@ -0,0 +1,28 @@ + + + diff --git a/packages/vue3/test-app/Pages/FormComponent/PrecognitionMethods.vue b/packages/vue3/test-app/Pages/FormComponent/PrecognitionMethods.vue new file mode 100644 index 000000000..aa1ba87ba --- /dev/null +++ b/packages/vue3/test-app/Pages/FormComponent/PrecognitionMethods.vue @@ -0,0 +1,55 @@ + + + diff --git a/packages/vue3/test-app/Pages/FormComponent/PrecognitionTransform.vue b/packages/vue3/test-app/Pages/FormComponent/PrecognitionTransform.vue new file mode 100644 index 000000000..03a97bc96 --- /dev/null +++ b/packages/vue3/test-app/Pages/FormComponent/PrecognitionTransform.vue @@ -0,0 +1,26 @@ + + + diff --git a/playgrounds/react/app/Http/Requests/PrecognitionFormRequest.php b/playgrounds/react/app/Http/Requests/PrecognitionFormRequest.php new file mode 100644 index 000000000..3fc8a6916 --- /dev/null +++ b/playgrounds/react/app/Http/Requests/PrecognitionFormRequest.php @@ -0,0 +1,32 @@ +|string> + */ + public function rules(): array + { + sleep(1); + + return [ + 'name' => ['required', 'string', 'min:3', 'max:255'], + 'email' => ['required', 'email', 'max:255'], + 'avatar' => ['nullable', 'file', 'image'], + ]; + } +} diff --git a/playgrounds/react/package.json b/playgrounds/react/package.json index 8efae28f1..ffa86dc14 100644 --- a/playgrounds/react/package.json +++ b/playgrounds/react/package.json @@ -11,18 +11,18 @@ "@inertiajs/react": "workspace:*", "@laravel/stream-react": "^0.3.9", "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.1.15", + "@tailwindcss/vite": "^4.1.16", "@types/react": "^19.2.2", "@types/react-dom": "^19.2.2", "@vitejs/plugin-react": "^4.7.0", "autosize": "^6.0.1", - "axios": "^1.12.2", + "axios": "^1.13.1", "laravel-vite-plugin": "^1.3.0", "lodash": "^4.17.21", "marked": "^16.4.1", "react": "^19.2.0", "react-dom": "^19.2.0", - "tailwindcss": "^4.1.15", + "tailwindcss": "^4.1.16", "typescript": "^4.9.5", "vite": "^6.4.1" } diff --git a/playgrounds/react/resources/js/Components/Layout.tsx b/playgrounds/react/resources/js/Components/Layout.tsx index ab6363618..8d5fa0119 100644 --- a/playgrounds/react/resources/js/Components/Layout.tsx +++ b/playgrounds/react/resources/js/Components/Layout.tsx @@ -17,10 +17,13 @@ export default function Layout({ children, padding = true }: { children: React.R Article - Form + useForm - Form Component + {'
'} + + + Precognition Async diff --git a/playgrounds/react/resources/js/Pages/FormComponentPrecognition.tsx b/playgrounds/react/resources/js/Pages/FormComponentPrecognition.tsx new file mode 100644 index 000000000..89d4b02cf --- /dev/null +++ b/playgrounds/react/resources/js/Pages/FormComponentPrecognition.tsx @@ -0,0 +1,271 @@ +import { FormComponentMethods } from '@inertiajs/core' +import { Form, Head } from '@inertiajs/react' +import { useState } from 'react' +import Layout from '../Components/Layout' + +const FormComponentPrecognition = () => { + const [callbacks, setCallbacks] = useState({ + success: false, + error: false, + finish: false, + }) + + const validateWithCallbacks = (validate: FormComponentMethods['validate']) => { + setCallbacks({ + success: false, + error: false, + finish: false, + }) + + validate({ + onPrecognitionSuccess: () => setCallbacks((prev) => ({ ...prev, success: true })), + onValidationError: () => setCallbacks((prev) => ({ ...prev, error: true })), + onFinish: () => setCallbacks((prev) => ({ ...prev, finish: true })), + onBeforeValidation: (newReq, oldReq) => { + // Prevent validation if name is 'block' + if (newReq.data?.name === 'block') { + alert('Validation blocked by onBefore!') + return false + } + }, + }) + } + + const [validateFiles, setValidateFiles] = useState(false) + const [validateTimeout, setValidateTimeout] = useState(1500) + + return ( + <> + +

Form Precognition

+ + {/* Live Validation & File Uploads */} +
+
+

Live Validation & File Uploads

+ + {/* Configuration Toggle */} +
+ +
+ + +
+
+ + + {({ errors, invalid, valid, validate, validating }) => ( + <> +

Validating: {validating ? ' Yes...' : ' No'}

+ +
+ + validate('name')} + className={`mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm ${ + invalid('name') ? 'border-red-500' : valid('name') ? 'border-green-500' : '' + }`} + /> + {invalid('name') &&

{errors.name}

} + {valid('name') &&

Valid!

} +
+ +
+ + validate('email')} + className={`mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm ${ + invalid('email') ? 'border-red-500' : valid('email') ? 'border-green-500' : '' + }`} + /> + {invalid('email') &&

{errors.email}

} + {valid('email') &&

Valid!

} +
+ +
+ + validate('avatar')} + className={`mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm ${ + invalid('avatar') ? 'border-red-500' : valid('avatar') ? 'border-green-500' : '' + }`} + /> + {invalid('avatar') &&

{errors.avatar}

} + {valid('avatar') &&

Valid!

} +

+ Files are validated during precognitive requests when validateFiles is enabled +

+
+ + )} + +
+ + {/* Touch & Reset Methods */} +
+

Touch & Reset Methods

+ +
+ {({ errors, invalid, validate, touch, touched, reset, validating }) => ( + <> +
+ + touch('name')} + className="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + /> + {invalid('name') &&

{errors.name}

} +

Touched: {String(touched('name'))}

+
+ +
+ + touch('email')} + className="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + /> + {invalid('email') &&

{errors.email}

} +

Touched: {String(touched('email'))}

+
+ + {validating &&

Validating...

} + +
+ + + +
+ +
+ Status: +
    +
  • Any field touched: {String(touched())}
  • +
  • Name touched: {String(touched('name'))}
  • +
  • Email touched: {String(touched('email'))}
  • +
+
+ + )} +
+
+ + {/* Validation Callbacks */} +
+

Validation Callbacks

+ +
+ {({ errors, invalid, validate, touch, validating }) => ( + <> +
+ + touch('name')} + className="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + /> + {invalid('name') &&

{errors.name}

} +
+ + {validating &&

Validating...

} + + {(callbacks.success || callbacks.error || callbacks.finish) && ( +
+ {callbacks.success &&

onPrecognitionSuccess called!

} + {callbacks.error &&

onValidationError called!

} + {callbacks.finish &&

onFinish called!

} +
+ )} + +
+ +
+ + )} +
+
+
+ + ) +} + +FormComponentPrecognition.layout = (page) => + +export default FormComponentPrecognition diff --git a/playgrounds/react/routes/web.php b/playgrounds/react/routes/web.php index 61aa69c20..36db775ce 100644 --- a/playgrounds/react/routes/web.php +++ b/playgrounds/react/routes/web.php @@ -1,6 +1,9 @@ validated(); + + // dd($data); + + return back(); +})->middleware([HandlePrecognitiveRequests::class]); + Route::post('/user', function () { return inertia('User', [ 'user' => request()->validate([ diff --git a/playgrounds/svelte4/app/Http/Requests/PrecognitionFormRequest.php b/playgrounds/svelte4/app/Http/Requests/PrecognitionFormRequest.php new file mode 100644 index 000000000..3fc8a6916 --- /dev/null +++ b/playgrounds/svelte4/app/Http/Requests/PrecognitionFormRequest.php @@ -0,0 +1,32 @@ +|string> + */ + public function rules(): array + { + sleep(1); + + return [ + 'name' => ['required', 'string', 'min:3', 'max:255'], + 'email' => ['required', 'email', 'max:255'], + 'avatar' => ['nullable', 'file', 'image'], + ]; + } +} diff --git a/playgrounds/svelte4/package.json b/playgrounds/svelte4/package.json index dfd2bbe84..a3c0f51d2 100644 --- a/playgrounds/svelte4/package.json +++ b/playgrounds/svelte4/package.json @@ -12,14 +12,14 @@ "@inertiajs/core": "workspace:*", "@inertiajs/svelte": "workspace:*", "@sveltejs/vite-plugin-svelte": "^3.1.2", - "@tailwindcss/vite": "^4.1.15", + "@tailwindcss/vite": "^4.1.16", "@tsconfig/svelte": "^5.0.5", - "axios": "^1.12.2", + "axios": "^1.13.1", "laravel-vite-plugin": "^1.3.0", "lodash": "^4.17.21", "svelte": "^4.2.20", "svelte-check": "^4.3.3", - "tailwindcss": "^4.1.15", + "tailwindcss": "^4.1.16", "typescript": "^5.9.3", "vite": "^5.4.21" } diff --git a/playgrounds/svelte4/resources/js/Components/Layout.svelte b/playgrounds/svelte4/resources/js/Components/Layout.svelte index e9e358dbf..02a2cd9b9 100644 --- a/playgrounds/svelte4/resources/js/Components/Layout.svelte +++ b/playgrounds/svelte4/resources/js/Components/Layout.svelte @@ -12,8 +12,9 @@ Home Users Article - Form - Form Component + useForm + {'
'} + Precognition External Async diff --git a/playgrounds/svelte4/resources/js/Pages/FormComponentPrecognition.svelte b/playgrounds/svelte4/resources/js/Pages/FormComponentPrecognition.svelte new file mode 100644 index 000000000..045e64e2e --- /dev/null +++ b/playgrounds/svelte4/resources/js/Pages/FormComponentPrecognition.svelte @@ -0,0 +1,279 @@ + + + + + + Precognition - {appName} + + +

Form Precognition

+ + +
+
+

Live Validation & File Uploads

+ + +
+ +
+ + +
+
+ + +

Validating: {validating ? 'Yes...' : 'No'}

+ +
+ + validate('name')} + class="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + class:border-red-500={invalid('name')} + class:border-green-500={valid('name')} + /> + {#if invalid('name')} +

{errors.name}

+ {/if} + {#if valid('name')} +

Valid!

+ {/if} +
+ +
+ + validate('email')} + class="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + class:border-red-500={invalid('email')} + class:border-green-500={valid('email')} + /> + {#if invalid('email')} +

{errors.email}

+ {/if} + {#if valid('email')} +

Valid!

+ {/if} +
+ +
+ + validate('avatar')} + class="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + class:border-red-500={invalid('avatar')} + class:border-green-500={valid('avatar')} + /> + {#if invalid('avatar')} +

{errors.avatar}

+ {/if} + {#if valid('avatar')} +

Valid!

+ {/if} +

+ Files are validated during precognitive requests when validateFiles is enabled +

+
+ +
+ + +
+

Touch & Reset Methods

+ +
+
+ + touch('name')} + class="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + /> + {#if invalid('name')} +

{errors.name}

+ {/if} +

Touched: {touched('name')}

+
+ +
+ + touch('email')} + class="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + /> + {#if invalid('email')} +

{errors.email}

+ {/if} +

Touched: {touched('email')}

+
+ + {#if validating} +

Validating...

+ {/if} + +
+ + + +
+ +
+ Status: +
    +
  • Any field touched: {touched()}
  • +
  • Name touched: {touched('name')}
  • +
  • Email touched: {touched('email')}
  • +
+
+
+
+ + +
+

Validation Callbacks

+ +
+
+ + touch('name')} + class="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + /> + {#if invalid('name')} +

{errors.name}

+ {/if} +
+ + {#if validating} +

Validating...

+ {/if} + + {#if callbacks.success || callbacks.error || callbacks.finish} +
+ {#if callbacks.success} +

onPrecognitionSuccess called!

+ {/if} + {#if callbacks.error} +

onValidationError called!

+ {/if} + {#if callbacks.finish} +

onFinish called!

+ {/if} +
+ {/if} + +
+ +
+
+
+
diff --git a/playgrounds/svelte4/routes/web.php b/playgrounds/svelte4/routes/web.php index b8029f3ec..0dec64e82 100644 --- a/playgrounds/svelte4/routes/web.php +++ b/playgrounds/svelte4/routes/web.php @@ -1,5 +1,7 @@ validated(); + + // dd($data); + + return back(); +})->middleware([HandlePrecognitiveRequests::class]); + Route::get('/photo-grid/{horizontal?}', function ($horizontal = null) { if (request()->header('X-Inertia-Partial-Component')) { // Simulate latency for partial reloads diff --git a/playgrounds/svelte5/app/Http/Requests/PrecognitionFormRequest.php b/playgrounds/svelte5/app/Http/Requests/PrecognitionFormRequest.php new file mode 100644 index 000000000..3fc8a6916 --- /dev/null +++ b/playgrounds/svelte5/app/Http/Requests/PrecognitionFormRequest.php @@ -0,0 +1,32 @@ +|string> + */ + public function rules(): array + { + sleep(1); + + return [ + 'name' => ['required', 'string', 'min:3', 'max:255'], + 'email' => ['required', 'email', 'max:255'], + 'avatar' => ['nullable', 'file', 'image'], + ]; + } +} diff --git a/playgrounds/svelte5/package.json b/playgrounds/svelte5/package.json index 43c6dc760..a45794054 100644 --- a/playgrounds/svelte5/package.json +++ b/playgrounds/svelte5/package.json @@ -12,14 +12,14 @@ "@inertiajs/core": "workspace:*", "@inertiajs/svelte": "workspace:*", "@sveltejs/vite-plugin-svelte": "^5.1.1", - "@tailwindcss/vite": "^4.1.15", + "@tailwindcss/vite": "^4.1.16", "@tsconfig/svelte": "^5.0.5", - "axios": "^1.12.2", + "axios": "^1.13.1", "laravel-vite-plugin": "^1.3.0", "lodash": "^4.17.21", - "svelte": "^5.41.1", + "svelte": "^5.43.2", "svelte-check": "^4.3.3", - "tailwindcss": "^4.1.15", + "tailwindcss": "^4.1.16", "typescript": "^5.9.3", "vite": "^6.4.1" } diff --git a/playgrounds/svelte5/resources/js/Components/Layout.svelte b/playgrounds/svelte5/resources/js/Components/Layout.svelte index f5ae9ec6d..a66a99102 100644 --- a/playgrounds/svelte5/resources/js/Components/Layout.svelte +++ b/playgrounds/svelte5/resources/js/Components/Layout.svelte @@ -9,8 +9,9 @@ Home Users Article - Form - Form Component + useForm + {'
'} + Precognition Photo Grid Photo Row Data Table diff --git a/playgrounds/svelte5/resources/js/Pages/FormComponentPrecognition.svelte b/playgrounds/svelte5/resources/js/Pages/FormComponentPrecognition.svelte new file mode 100644 index 000000000..ea2a8c69c --- /dev/null +++ b/playgrounds/svelte5/resources/js/Pages/FormComponentPrecognition.svelte @@ -0,0 +1,258 @@ + + + + + + Precognition - {appName} + + +

Form Precognition

+ + +
+
+

Live Validation & File Uploads

+ + +
+ +
+ + +
+
+ + + {#snippet children({ errors, invalid, valid, validate, validating })} +

Validating: {validating ? 'Yes...' : 'No'}

+ +
+ + validate('name')} + class="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + class:border-red-500={invalid('name')} + class:border-green-500={valid('name')} + /> + {#if invalid('name')} +

{errors.name}

+ {/if} + {#if valid('name')} +

Valid!

+ {/if} +
+ +
+ + validate('email')} + class="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + class:border-red-500={invalid('email')} + class:border-green-500={valid('email')} + /> + {#if invalid('email')} +

{errors.email}

+ {/if} + {#if valid('email')} +

Valid!

+ {/if} +
+ +
+ + validate('avatar')} + class="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + class:border-red-500={invalid('avatar')} + class:border-green-500={valid('avatar')} + /> + {#if invalid('avatar')} +

{errors.avatar}

+ {/if} + {#if valid('avatar')} +

Valid!

+ {/if} +

+ Files are validated during precognitive requests when validateFiles is enabled +

+
+ {/snippet} + +
+ + +
+

Touch & Reset Methods

+ +
+ {#snippet children({ errors, invalid, validate, touch, touched, reset, validating })} +
+ + touch('name')} + class="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + /> + {#if invalid('name')} +

{errors.name}

+ {/if} +

Touched: {touched('name')}

+
+ +
+ + touch('email')} + class="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + /> + {#if invalid('email')} +

{errors.email}

+ {/if} +

Touched: {touched('email')}

+
+ + {#if validating} +

Validating...

+ {/if} + +
+ + + +
+ +
+ Status: +
    +
  • Any field touched: {touched()}
  • +
  • Name touched: {touched('name')}
  • +
  • Email touched: {touched('email')}
  • +
+
+ {/snippet} +
+
+ + +
+

Validation Callbacks

+ +
+ {#snippet children({ errors, invalid, validate, touch, validating })} +
+ + touch('name')} + class="mt-1 w-full appearance-none rounded border px-2 py-1 shadow-sm" + /> + {#if invalid('name')} +

{errors.name}

+ {/if} +
+ + {#if validating} +

Validating...

+ {/if} + + {#if callbacks.success || callbacks.error || callbacks.finish} +
+ {#if callbacks.success} +

onPrecognitionSuccess called!

+ {/if} + {#if callbacks.error} +

onValidationError called!

+ {/if} + {#if callbacks.finish} +

onFinish called!

+ {/if} +
+ {/if} + +
+ +
+ {/snippet} +
+
+
diff --git a/playgrounds/svelte5/routes/web.php b/playgrounds/svelte5/routes/web.php index 29df038d2..28c3938ca 100644 --- a/playgrounds/svelte5/routes/web.php +++ b/playgrounds/svelte5/routes/web.php @@ -1,5 +1,7 @@ validated(); + + // dd($data); + + return back(); +})->middleware([HandlePrecognitiveRequests::class]); + Route::get('/photo-grid/{horizontal?}', function ($horizontal = null) { if (request()->header('X-Inertia-Partial-Component')) { // Simulate latency for partial reloads diff --git a/playgrounds/vue3/app/Http/Requests/PrecognitionFormRequest.php b/playgrounds/vue3/app/Http/Requests/PrecognitionFormRequest.php new file mode 100644 index 000000000..3fc8a6916 --- /dev/null +++ b/playgrounds/vue3/app/Http/Requests/PrecognitionFormRequest.php @@ -0,0 +1,32 @@ +|string> + */ + public function rules(): array + { + sleep(1); + + return [ + 'name' => ['required', 'string', 'min:3', 'max:255'], + 'email' => ['required', 'email', 'max:255'], + 'avatar' => ['nullable', 'file', 'image'], + ]; + } +} diff --git a/playgrounds/vue3/package.json b/playgrounds/vue3/package.json index ced3a6aa9..611843520 100644 --- a/playgrounds/vue3/package.json +++ b/playgrounds/vue3/package.json @@ -10,15 +10,15 @@ "@inertiajs/vue3": "workspace:*", "@laravel/stream-vue": "^0.3.9", "@tailwindcss/typography": "^0.5.19", - "@tailwindcss/vite": "^4.1.15", + "@tailwindcss/vite": "^4.1.16", "@vitejs/plugin-vue": "^5.2.4", "@vue/server-renderer": "^3.5.22", "autosize": "^6.0.1", - "axios": "^1.12.2", + "axios": "^1.13.1", "laravel-vite-plugin": "^1.3.0", "lodash": "^4.17.21", "marked": "^16.4.1", - "tailwindcss": "^4.1.15", + "tailwindcss": "^4.1.16", "typescript": "^5.9.3", "vite": "^6.4.1", "vue": "^3.5.22", diff --git a/playgrounds/vue3/resources/js/Components/Layout.vue b/playgrounds/vue3/resources/js/Components/Layout.vue index 88c5bea06..8da94e29a 100644 --- a/playgrounds/vue3/resources/js/Components/Layout.vue +++ b/playgrounds/vue3/resources/js/Components/Layout.vue @@ -22,8 +22,9 @@ const appName = computed(() => page.props.appName) Home Users Article - Form - Form Component + useForm + {{ '<' + 'Form' + '>' }} + Precognition Logout External Async diff --git a/playgrounds/vue3/resources/js/Pages/FormComponentPrecognition.vue b/playgrounds/vue3/resources/js/Pages/FormComponentPrecognition.vue new file mode 100644 index 000000000..ef102ca40 --- /dev/null +++ b/playgrounds/vue3/resources/js/Pages/FormComponentPrecognition.vue @@ -0,0 +1,227 @@ + + + + + diff --git a/playgrounds/vue3/routes/web.php b/playgrounds/vue3/routes/web.php index efa24a69e..11af53004 100644 --- a/playgrounds/vue3/routes/web.php +++ b/playgrounds/vue3/routes/web.php @@ -1,6 +1,8 @@ validated(); + + // dd($data); + + return back(); +})->middleware([HandlePrecognitiveRequests::class]); + Route::post('/user', function () { return inertia('User', [ 'user' => request()->validate([ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3945c6520..96ccd6cbf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -19,10 +19,10 @@ importers: version: 4.3.0(prettier@3.6.2)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3)) prettier-plugin-svelte: specifier: ^3.4.0 - version: 3.4.0(prettier@3.6.2)(svelte@5.41.1) + version: 3.4.0(prettier@3.6.2)(svelte@5.43.2) prettier-plugin-tailwindcss: specifier: ^0.6.14 - version: 0.6.14(prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3)))(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.41.1))(prettier@3.6.2) + version: 0.6.14(prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3)))(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.43.2))(prettier@3.6.2) optionalDependencies: '@rollup/rollup-linux-x64-gnu': specifier: ^4.52.5 @@ -34,8 +34,11 @@ importers: specifier: ^4.17.12 version: 4.17.12 axios: - specifier: ^1.12.2 - version: 1.12.2 + specifier: ^1.13.1 + version: 1.13.1 + laravel-precognition: + specifier: ^0.7.3 + version: 0.7.3 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -76,6 +79,9 @@ importers: '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 + laravel-precognition: + specifier: ^0.7.3 + version: 0.7.3 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -87,8 +93,8 @@ importers: specifier: ^19.2.2 version: 19.2.2(@types/react@19.2.2) axios: - specifier: ^1.12.2 - version: 1.12.2 + specifier: ^1.13.1 + version: 1.13.1 es-check: specifier: ^9.4.4 version: 9.4.4 @@ -115,7 +121,7 @@ importers: version: link:.. '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) + version: 4.7.0(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) react: specifier: ^19.2.0 version: 19.2.0 @@ -127,8 +133,8 @@ importers: specifier: ^9.38.0 version: 9.38.0 '@types/node': - specifier: ^24.9.1 - version: 24.9.1 + specifier: ^24.9.2 + version: 24.9.2 '@types/react': specifier: ^19.2.2 version: 19.2.2 @@ -161,7 +167,7 @@ importers: version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) + version: 6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) packages/svelte: dependencies: @@ -171,25 +177,28 @@ importers: '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 + laravel-precognition: + specifier: ^0.7.3 + version: 0.7.3 lodash-es: specifier: ^4.17.21 version: 4.17.21 devDependencies: '@sveltejs/adapter-auto': specifier: ^3.3.1 - version: 3.3.1(@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0))) + version: 3.3.1(@sveltejs/kit@2.48.3(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0))) '@sveltejs/kit': - specifier: ^2.47.2 - version: 2.47.2(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)) + specifier: ^2.48.3 + version: 2.48.3(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)) '@sveltejs/package': specifier: ^2.5.4 version: 2.5.4(svelte@4.2.20)(typescript@5.9.3) '@sveltejs/vite-plugin-svelte': specifier: ^3.1.2 - version: 3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)) + version: 3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)) axios: - specifier: ^1.12.2 - version: 1.12.2 + specifier: ^1.13.1 + version: 1.13.1 es-check: specifier: ^9.4.4 version: 9.4.4 @@ -210,7 +219,7 @@ importers: version: 5.9.3 vite: specifier: ^5.4.21 - version: 5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0) + version: 5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0) packages/svelte/test-app: dependencies: @@ -226,10 +235,10 @@ importers: version: 9.38.0 '@sveltejs/eslint-config': specifier: ^8.3.4 - version: 8.3.4(@stylistic/eslint-plugin-js@4.4.1(eslint@9.38.0(jiti@2.6.1)))(eslint-config-prettier@10.1.8(eslint@9.38.0(jiti@2.6.1)))(eslint-plugin-n@17.23.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-plugin-svelte@3.12.5(eslint@9.38.0(jiti@2.6.1))(svelte@4.2.20))(eslint@9.38.0(jiti@2.6.1))(typescript-eslint@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(typescript@5.9.3) + version: 8.3.4(@stylistic/eslint-plugin-js@4.4.1(eslint@9.38.0(jiti@2.6.1)))(eslint-config-prettier@10.1.8(eslint@9.38.0(jiti@2.6.1)))(eslint-plugin-n@17.23.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-plugin-svelte@3.13.0(eslint@9.38.0(jiti@2.6.1))(svelte@4.2.20))(eslint@9.38.0(jiti@2.6.1))(typescript-eslint@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(typescript@5.9.3) '@sveltejs/vite-plugin-svelte': specifier: ^3.1.2 - version: 3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)) + version: 3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)) '@tsconfig/svelte': specifier: ^5.0.5 version: 5.0.5 @@ -237,8 +246,8 @@ importers: specifier: ^9.38.0 version: 9.38.0(jiti@2.6.1) eslint-plugin-svelte: - specifier: ^3.12.5 - version: 3.12.5(eslint@9.38.0(jiti@2.6.1))(svelte@4.2.20) + specifier: ^3.13.0 + version: 3.13.0(eslint@9.38.0(jiti@2.6.1))(svelte@4.2.20) globals: specifier: ^16.4.0 version: 16.4.0 @@ -259,7 +268,7 @@ importers: version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^5.4.21 - version: 5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0) + version: 5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0) packages/vue3: dependencies: @@ -269,16 +278,19 @@ importers: '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 + laravel-precognition: + specifier: ^0.7.3 + version: 0.7.3 lodash-es: specifier: ^4.17.21 version: 4.17.21 devDependencies: '@types/node': - specifier: ^22.18.12 - version: 22.18.12 + specifier: ^22.18.13 + version: 22.18.13 axios: - specifier: ^1.12.2 - version: 1.12.2 + specifier: ^1.13.1 + version: 1.13.1 es-check: specifier: ^9.4.4 version: 9.4.4 @@ -309,7 +321,7 @@ importers: version: 9.38.0 '@vitejs/plugin-vue': specifier: ^5.2.4 - version: 5.2.4(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))(vue@3.5.22(typescript@5.9.3)) + version: 5.2.4(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))(vue@3.5.22(typescript@5.9.3)) '@vue/eslint-config-typescript': specifier: ^14.6.0 version: 14.6.0(eslint-plugin-vue@10.5.1(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.38.0(jiti@2.6.1))))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) @@ -336,7 +348,7 @@ importers: version: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) + version: 6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) vue: specifier: ^3.5.22 version: 3.5.22(typescript@5.9.3) @@ -357,10 +369,10 @@ importers: version: 0.3.9(react@19.2.0) '@tailwindcss/typography': specifier: ^0.5.19 - version: 0.5.19(tailwindcss@4.1.15) + version: 0.5.19(tailwindcss@4.1.16) '@tailwindcss/vite': - specifier: ^4.1.15 - version: 4.1.15(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) + specifier: ^4.1.16 + version: 4.1.16(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) '@types/react': specifier: ^19.2.2 version: 19.2.2 @@ -369,16 +381,16 @@ importers: version: 19.2.2(@types/react@19.2.2) '@vitejs/plugin-react': specifier: ^4.7.0 - version: 4.7.0(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) + version: 4.7.0(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) autosize: specifier: ^6.0.1 version: 6.0.1 axios: - specifier: ^1.12.2 - version: 1.12.2 + specifier: ^1.13.1 + version: 1.13.1 laravel-vite-plugin: specifier: ^1.3.0 - version: 1.3.0(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) + version: 1.3.0(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) lodash: specifier: ^4.17.21 version: 4.17.21 @@ -392,14 +404,14 @@ importers: specifier: ^19.2.0 version: 19.2.0(react@19.2.0) tailwindcss: - specifier: ^4.1.15 - version: 4.1.15 + specifier: ^4.1.16 + version: 4.1.16 typescript: specifier: ^4.9.5 version: 4.9.5 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) + version: 6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) playgrounds/svelte4: devDependencies: @@ -411,19 +423,19 @@ importers: version: link:../../packages/svelte '@sveltejs/vite-plugin-svelte': specifier: ^3.1.2 - version: 3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)) + version: 3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)) '@tailwindcss/vite': - specifier: ^4.1.15 - version: 4.1.15(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)) + specifier: ^4.1.16 + version: 4.1.16(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)) '@tsconfig/svelte': specifier: ^5.0.5 version: 5.0.5 axios: - specifier: ^1.12.2 - version: 1.12.2 + specifier: ^1.13.1 + version: 1.13.1 laravel-vite-plugin: specifier: ^1.3.0 - version: 1.3.0(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)) + version: 1.3.0(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)) lodash: specifier: ^4.17.21 version: 4.17.21 @@ -434,14 +446,14 @@ importers: specifier: ^4.3.3 version: 4.3.3(picomatch@4.0.3)(svelte@4.2.20)(typescript@5.9.3) tailwindcss: - specifier: ^4.1.15 - version: 4.1.15 + specifier: ^4.1.16 + version: 4.1.16 typescript: specifier: ^5.9.3 version: 5.9.3 vite: specifier: ^5.4.21 - version: 5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0) + version: 5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0) playgrounds/svelte5: devDependencies: @@ -453,37 +465,37 @@ importers: version: link:../../packages/svelte '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.41.1)(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) + version: 5.1.1(svelte@5.43.2)(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) '@tailwindcss/vite': - specifier: ^4.1.15 - version: 4.1.15(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) + specifier: ^4.1.16 + version: 4.1.16(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) '@tsconfig/svelte': specifier: ^5.0.5 version: 5.0.5 axios: - specifier: ^1.12.2 - version: 1.12.2 + specifier: ^1.13.1 + version: 1.13.1 laravel-vite-plugin: specifier: ^1.3.0 - version: 1.3.0(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) + version: 1.3.0(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) lodash: specifier: ^4.17.21 version: 4.17.21 svelte: - specifier: ^5.41.1 - version: 5.41.1 + specifier: ^5.43.2 + version: 5.43.2 svelte-check: specifier: ^4.3.3 - version: 4.3.3(picomatch@4.0.3)(svelte@5.41.1)(typescript@5.9.3) + version: 4.3.3(picomatch@4.0.3)(svelte@5.43.2)(typescript@5.9.3) tailwindcss: - specifier: ^4.1.15 - version: 4.1.15 + specifier: ^4.1.16 + version: 4.1.16 typescript: specifier: ^5.9.3 version: 5.9.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) + version: 6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) playgrounds/vue3: devDependencies: @@ -498,13 +510,13 @@ importers: version: 0.3.9(vue@3.5.22(typescript@5.9.3)) '@tailwindcss/typography': specifier: ^0.5.19 - version: 0.5.19(tailwindcss@4.1.15) + version: 0.5.19(tailwindcss@4.1.16) '@tailwindcss/vite': - specifier: ^4.1.15 - version: 4.1.15(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) + specifier: ^4.1.16 + version: 4.1.16(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) '@vitejs/plugin-vue': specifier: ^5.2.4 - version: 5.2.4(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))(vue@3.5.22(typescript@5.9.3)) + version: 5.2.4(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))(vue@3.5.22(typescript@5.9.3)) '@vue/server-renderer': specifier: ^3.5.22 version: 3.5.22(vue@3.5.22(typescript@5.9.3)) @@ -512,11 +524,11 @@ importers: specifier: ^6.0.1 version: 6.0.1 axios: - specifier: ^1.12.2 - version: 1.12.2 + specifier: ^1.13.1 + version: 1.13.1 laravel-vite-plugin: specifier: ^1.3.0 - version: 1.3.0(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) + version: 1.3.0(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) lodash: specifier: ^4.17.21 version: 4.17.21 @@ -524,14 +536,14 @@ importers: specifier: ^16.4.1 version: 16.4.1 tailwindcss: - specifier: ^4.1.15 - version: 4.1.15 + specifier: ^4.1.16 + version: 4.1.16 typescript: specifier: ^5.9.3 version: 5.9.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) + version: 6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) vue: specifier: ^3.5.22 version: 3.5.22(typescript@5.9.3) @@ -573,16 +585,16 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.28.4': - resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + '@babel/compat-data@7.28.5': + resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} - '@babel/core@7.28.4': - resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + '@babel/core@7.28.5': + resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==} engines: {node: '>=6.9.0'} - '@babel/generator@7.28.3': - resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + '@babel/generator@7.28.5': + resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.27.2': @@ -611,8 +623,8 @@ packages: resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.27.1': - resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.27.1': @@ -627,8 +639,8 @@ packages: resolution: {integrity: sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==} engines: {node: '>=6.9.0'} - '@babel/parser@7.28.4': - resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -648,12 +660,12 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.28.4': - resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + '@babel/traverse@7.28.5': + resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} - '@babel/types@7.28.4': - resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} '@colors/colors@1.6.0': @@ -963,22 +975,26 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@eslint-community/regexpp@4.12.1': - resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} '@eslint/config-array@0.21.1': resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/config-helpers@0.4.1': - resolution: {integrity: sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==} + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/core@0.16.0': resolution: {integrity: sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/eslintrc@0.4.3': resolution: {integrity: sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==} engines: {node: ^10.12.0 || >=12.0.0} @@ -995,8 +1011,8 @@ packages: resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.4.0': - resolution: {integrity: sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==} + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@humanfs/core@0.19.1': @@ -1219,8 +1235,8 @@ packages: typescript: '>= 5' typescript-eslint: '>= 8' - '@sveltejs/kit@2.47.2': - resolution: {integrity: sha512-mbUomaJTiADTrq6GT4ZvQ7v1rs0S+wXGMzrjFwjARAKMEF8FpOUmz2uEJ4M9WMJMQOXCMHpKFzJfdjo9O7M22A==} + '@sveltejs/kit@2.48.3': + resolution: {integrity: sha512-jf8mx3yctRXE9hvixgcqqK94YI2hDnbxI/12Upkz99XFMvxnJKCMzvz0j7lmbXSyBSNEycWO5xHvi7b73y9qkQ==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -1269,65 +1285,65 @@ packages: svelte: ^5.0.0 vite: ^6.0.0 - '@tailwindcss/node@4.1.15': - resolution: {integrity: sha512-HF4+7QxATZWY3Jr8OlZrBSXmwT3Watj0OogeDvdUY/ByXJHQ+LBtqA2brDb3sBxYslIFx6UP94BJ4X6a4L9Bmw==} + '@tailwindcss/node@4.1.16': + resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==} - '@tailwindcss/oxide-android-arm64@4.1.15': - resolution: {integrity: sha512-TkUkUgAw8At4cBjCeVCRMc/guVLKOU1D+sBPrHt5uVcGhlbVKxrCaCW9OKUIBv1oWkjh4GbunD/u/Mf0ql6kEA==} + '@tailwindcss/oxide-android-arm64@4.1.16': + resolution: {integrity: sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@tailwindcss/oxide-darwin-arm64@4.1.15': - resolution: {integrity: sha512-xt5XEJpn2piMSfvd1UFN6jrWXyaKCwikP4Pidcf+yfHTSzSpYhG3dcMktjNkQO3JiLCp+0bG0HoWGvz97K162w==} + '@tailwindcss/oxide-darwin-arm64@4.1.16': + resolution: {integrity: sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@tailwindcss/oxide-darwin-x64@4.1.15': - resolution: {integrity: sha512-TnWaxP6Bx2CojZEXAV2M01Yl13nYPpp0EtGpUrY+LMciKfIXiLL2r/SiSRpagE5Fp2gX+rflp/Os1VJDAyqymg==} + '@tailwindcss/oxide-darwin-x64@4.1.16': + resolution: {integrity: sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@tailwindcss/oxide-freebsd-x64@4.1.15': - resolution: {integrity: sha512-quISQDWqiB6Cqhjc3iWptXVZHNVENsWoI77L1qgGEHNIdLDLFnw3/AfY7DidAiiCIkGX/MjIdB3bbBZR/G2aJg==} + '@tailwindcss/oxide-freebsd-x64@4.1.16': + resolution: {integrity: sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.15': - resolution: {integrity: sha512-ObG76+vPlab65xzVUQbExmDU9FIeYLQ5k2LrQdR2Ud6hboR+ZobXpDoKEYXf/uOezOfIYmy2Ta3w0ejkTg9yxg==} + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': + resolution: {integrity: sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@tailwindcss/oxide-linux-arm64-gnu@4.1.15': - resolution: {integrity: sha512-4WbBacRmk43pkb8/xts3wnOZMDKsPFyEH/oisCm2q3aLZND25ufvJKcDUpAu0cS+CBOL05dYa8D4U5OWECuH/Q==} + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': + resolution: {integrity: sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-arm64-musl@4.1.15': - resolution: {integrity: sha512-AbvmEiteEj1nf42nE8skdHv73NoR+EwXVSgPY6l39X12Ex8pzOwwfi3Kc8GAmjsnsaDEbk+aj9NyL3UeyHcTLg==} + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': + resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - '@tailwindcss/oxide-linux-x64-gnu@4.1.15': - resolution: {integrity: sha512-+rzMVlvVgrXtFiS+ES78yWgKqpThgV19ISKD58Ck+YO5pO5KjyxLt7AWKsWMbY0R9yBDC82w6QVGz837AKQcHg==} + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': + resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-linux-x64-musl@4.1.15': - resolution: {integrity: sha512-fPdEy7a8eQN9qOIK3Em9D3TO1z41JScJn8yxl/76mp4sAXFDfV4YXxsiptJcOwy6bGR+70ZSwFIZhTXzQeqwQg==} + '@tailwindcss/oxide-linux-x64-musl@4.1.16': + resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - '@tailwindcss/oxide-wasm32-wasi@4.1.15': - resolution: {integrity: sha512-sJ4yd6iXXdlgIMfIBXuVGp/NvmviEoMVWMOAGxtxhzLPp9LOj5k0pMEMZdjeMCl4C6Up+RM8T3Zgk+BMQ0bGcQ==} + '@tailwindcss/oxide-wasm32-wasi@4.1.16': + resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==} engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: @@ -1338,20 +1354,20 @@ packages: - '@emnapi/wasi-threads' - tslib - '@tailwindcss/oxide-win32-arm64-msvc@4.1.15': - resolution: {integrity: sha512-sJGE5faXnNQ1iXeqmRin7Ds/ru2fgCiaQZQQz3ZGIDtvbkeV85rAZ0QJFMDg0FrqsffZG96H1U9AQlNBRLsHVg==} + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': + resolution: {integrity: sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@tailwindcss/oxide-win32-x64-msvc@4.1.15': - resolution: {integrity: sha512-NLeHE7jUV6HcFKS504bpOohyi01zPXi2PXmjFfkzTph8xRxDdxkRsXm/xDO5uV5K3brrE1cCwbUYmFUSHR3u1w==} + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': + resolution: {integrity: sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@tailwindcss/oxide@4.1.15': - resolution: {integrity: sha512-krhX+UOOgnsUuks2SR7hFafXmLQrKxB4YyRTERuCE59JlYL+FawgaAlSkOYmDRJdf1Q+IFNDMl9iRnBW7QBDfQ==} + '@tailwindcss/oxide@4.1.16': + resolution: {integrity: sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==} engines: {node: '>= 10'} '@tailwindcss/typography@0.5.19': @@ -1359,8 +1375,8 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' - '@tailwindcss/vite@4.1.15': - resolution: {integrity: sha512-B6s60MZRTUil+xKoZoGe6i0Iar5VuW+pmcGlda2FX+guDuQ1G1sjiIy1W0frneVpeL/ZjZ4KEgWZHNrIm++2qA==} + '@tailwindcss/vite@4.1.16': + resolution: {integrity: sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==} peerDependencies: vite: ^5.2.0 || ^6 || ^7 @@ -1407,11 +1423,11 @@ packages: '@types/node@18.19.130': resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} - '@types/node@22.18.12': - resolution: {integrity: sha512-BICHQ67iqxQGFSzfCFTT7MRQ5XcBjG5aeKh5Ok38UBbPe5fxTyE+aHFxwVrGyr8GNlqFMLKD1D3P2K/1ks8tog==} + '@types/node@22.18.13': + resolution: {integrity: sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==} - '@types/node@24.9.1': - resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==} + '@types/node@24.9.2': + resolution: {integrity: sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==} '@types/nprogress@0.2.3': resolution: {integrity: sha512-k7kRA033QNtC+gLc4VPlfnue58CM1iQLgn1IMAU8VPHGOj7oIHPp9UlhedEnD/Gl8evoCjwkZjlBORtZ3JByUA==} @@ -1754,8 +1770,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axios@1.12.2: - resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + axios@1.13.1: + resolution: {integrity: sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==} axobject-query@4.1.0: resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} @@ -1764,8 +1780,8 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - baseline-browser-mapping@2.8.18: - resolution: {integrity: sha512-UYmTpOBwgPScZpS4A+YbapwWuBwasxvO/2IOHArSsAhL/+ZdmATBXTex3t+l2hXwLVYK382ibr/nKoY9GKe86w==} + baseline-browser-mapping@2.8.21: + resolution: {integrity: sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==} hasBin: true big.js@5.2.2: @@ -1792,8 +1808,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.26.3: - resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} + browserslist@4.27.0: + resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -2011,8 +2027,8 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.4.1: - resolution: {integrity: sha512-YtoaOfsqjbZQKGIMRYDWKjUmSB4VJ/RElB+bXZawQAQYAo4xu08GKTMVlsZDTF6R2MbAgjcAQRPI5eIyRAT2OQ==} + devalue@5.4.2: + resolution: {integrity: sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw==} doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} @@ -2029,8 +2045,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.237: - resolution: {integrity: sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==} + electron-to-chromium@1.5.243: + resolution: {integrity: sha512-ZCphxFW3Q1TVhcgS9blfut1PX8lusVi2SvXQgmEEnK4TCmE1JhH2JkjJN+DNt0pJJwfBri5AROBnz2b/C+YU9g==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -2169,8 +2185,8 @@ packages: peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 - eslint-plugin-svelte@3.12.5: - resolution: {integrity: sha512-4KRG84eAHQfYd9OjZ1K7sCHy0nox+9KwT+s5WCCku3jTim5RV4tVENob274nCwIaApXsYPKAUAZFBxKZ3Wyfjw==} + eslint-plugin-svelte@3.13.0: + resolution: {integrity: sha512-2ohCCQJJTNbIpQCSDSTWj+FN0OVfPmSO03lmSNT7ytqMaWF6kpT86LdzDqtm4sh7TVPl/OEWJ/d7R87bXP2Vjg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.1 || ^9.0.0 @@ -2257,8 +2273,8 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} - esrap@2.1.0: - resolution: {integrity: sha512-yzmPNpl7TBbMRC5Lj2JlJZNPml0tzqoqP5B1JXycNUwtqma9AKCO0M2wHrdgsHcy1WRW7S9rJknAMtByg3usgA==} + esrap@2.1.1: + resolution: {integrity: sha512-ebTT9B6lOtZGMgJ3o5r12wBacHctG7oEWazIda8UlPfA3HD/Wrv8FdXoVo73vzdpwCxNyXjPauyN2bbJzMkB9A==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -2433,8 +2449,8 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} - get-tsconfig@4.12.0: - resolution: {integrity: sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==} + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -2769,6 +2785,9 @@ packages: kuler@2.0.0: resolution: {integrity: sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==} + laravel-precognition@0.7.3: + resolution: {integrity: sha512-Z97i35Q0wmsRC9WFUY2EmFS2W1F6FF/HakwZg6PqSaR9lHBgbdIs1YZpDlkChVkgcmmvbN19ujKss3ZqbjPM1g==} + laravel-vite-plugin@1.3.0: resolution: {integrity: sha512-P5qyG56YbYxM8OuYmK2OkhcKe0AksNVJUjq9LUZ5tOekU9fBn9LujYyctI4t9XoLjuMvHJXXpCoPntY1oKltuA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -2896,8 +2915,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - magic-string@0.30.19: - resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} marked@16.4.1: resolution: {integrity: sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg==} @@ -3005,8 +3024,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-releases@2.0.26: - resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} nodemon@3.1.10: resolution: {integrity: sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw==} @@ -3477,8 +3496,8 @@ packages: resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==} engines: {node: '>= 0.8.0'} - set-cookie-parser@2.7.1: - resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} @@ -3647,16 +3666,16 @@ packages: resolution: {integrity: sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==} engines: {node: '>=16'} - svelte@5.41.1: - resolution: {integrity: sha512-0a/huwc8e2es+7KFi70esqsReRfRbrT8h1cJSY/+z1lF0yKM6TT+//HYu28Yxstr50H7ifaqZRDGd0KuKDxP7w==} + svelte@5.43.2: + resolution: {integrity: sha512-ro1umEzX8rT5JpCmlf0PPv7ncD8MdVob9e18bhwqTKNoLjS8kDvhVpaoYVPc+qMwDAOfcwJtyY7ZFSDbOaNPgA==} engines: {node: '>=18'} table@6.9.0: resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==} engines: {node: '>=10.0.0'} - tailwindcss@4.1.15: - resolution: {integrity: sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==} + tailwindcss@4.1.16: + resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==} tapable@2.3.0: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} @@ -3795,8 +3814,8 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -4020,23 +4039,23 @@ snapshots: '@babel/code-frame@7.27.1': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/compat-data@7.28.4': {} + '@babel/compat-data@7.28.5': {} - '@babel/core@7.28.4': + '@babel/core@7.28.5': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-compilation-targets': 7.27.2 - '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5) '@babel/helpers': 7.28.4 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3(supports-color@5.5.0) @@ -4046,19 +4065,19 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.28.3': + '@babel/generator@7.28.5': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 '@babel/helper-compilation-targets@7.27.2': dependencies: - '@babel/compat-data': 7.28.4 + '@babel/compat-data': 7.28.5 '@babel/helper-validator-option': 7.27.1 - browserslist: 4.26.3 + browserslist: 4.27.0 lru-cache: 5.1.1 semver: 6.3.1 @@ -4066,17 +4085,17 @@ snapshots: '@babel/helper-module-imports@7.27.1': dependencies: - '@babel/traverse': 7.28.4 - '@babel/types': 7.28.4 + '@babel/traverse': 7.28.5 + '@babel/types': 7.28.5 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-module-imports': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 - '@babel/traverse': 7.28.4 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.5 transitivePeerDependencies: - supports-color @@ -4084,58 +4103,58 @@ snapshots: '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-identifier@7.27.1': {} + '@babel/helper-validator-identifier@7.28.5': {} '@babel/helper-validator-option@7.27.1': {} '@babel/helpers@7.28.4': dependencies: '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@babel/highlight@7.25.9': dependencies: - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 chalk: 2.4.2 js-tokens: 4.0.0 picocolors: 1.1.1 - '@babel/parser@7.28.4': + '@babel/parser@7.28.5': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.4)': + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)': dependencies: - '@babel/core': 7.28.4 + '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 '@babel/template@7.27.2': dependencies: '@babel/code-frame': 7.27.1 - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 - '@babel/traverse@7.28.4': + '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 - '@babel/generator': 7.28.3 + '@babel/generator': 7.28.5 '@babel/helper-globals': 7.28.0 - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@babel/template': 7.27.2 - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 debug: 4.4.3(supports-color@5.5.0) transitivePeerDependencies: - supports-color - '@babel/types@7.28.4': + '@babel/types@7.28.5': dependencies: '@babel/helper-string-parser': 7.27.1 - '@babel/helper-validator-identifier': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 '@colors/colors@1.6.0': {} @@ -4297,7 +4316,7 @@ snapshots: eslint: 9.38.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 - '@eslint-community/regexpp@4.12.1': {} + '@eslint-community/regexpp@4.12.2': {} '@eslint/config-array@0.21.1': dependencies: @@ -4307,14 +4326,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/config-helpers@0.4.1': + '@eslint/config-helpers@0.4.2': dependencies: - '@eslint/core': 0.16.0 + '@eslint/core': 0.17.0 '@eslint/core@0.16.0': dependencies: '@types/json-schema': 7.0.15 + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + '@eslint/eslintrc@0.4.3': dependencies: ajv: 6.12.6 @@ -4347,9 +4370,9 @@ snapshots: '@eslint/object-schema@2.1.7': {} - '@eslint/plugin-kit@0.4.0': + '@eslint/plugin-kit@0.4.1': dependencies: - '@eslint/core': 0.16.0 + '@eslint/core': 0.17.0 levn: 0.4.1 '@humanfs/core@0.19.1': {} @@ -4509,40 +4532,40 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)))': + '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.48.3(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)))': dependencies: - '@sveltejs/kit': 2.47.2(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)) + '@sveltejs/kit': 2.48.3(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)) import-meta-resolve: 4.2.0 - '@sveltejs/eslint-config@8.3.4(@stylistic/eslint-plugin-js@4.4.1(eslint@9.38.0(jiti@2.6.1)))(eslint-config-prettier@10.1.8(eslint@9.38.0(jiti@2.6.1)))(eslint-plugin-n@17.23.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-plugin-svelte@3.12.5(eslint@9.38.0(jiti@2.6.1))(svelte@4.2.20))(eslint@9.38.0(jiti@2.6.1))(typescript-eslint@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(typescript@5.9.3)': + '@sveltejs/eslint-config@8.3.4(@stylistic/eslint-plugin-js@4.4.1(eslint@9.38.0(jiti@2.6.1)))(eslint-config-prettier@10.1.8(eslint@9.38.0(jiti@2.6.1)))(eslint-plugin-n@17.23.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint-plugin-svelte@3.13.0(eslint@9.38.0(jiti@2.6.1))(svelte@4.2.20))(eslint@9.38.0(jiti@2.6.1))(typescript-eslint@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(typescript@5.9.3)': dependencies: '@stylistic/eslint-plugin-js': 4.4.1(eslint@9.38.0(jiti@2.6.1)) eslint: 9.38.0(jiti@2.6.1) eslint-config-prettier: 10.1.8(eslint@9.38.0(jiti@2.6.1)) eslint-plugin-n: 17.23.1(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-svelte: 3.12.5(eslint@9.38.0(jiti@2.6.1))(svelte@4.2.20) + eslint-plugin-svelte: 3.13.0(eslint@9.38.0(jiti@2.6.1))(svelte@4.2.20) globals: 15.15.0 typescript: 5.9.3 typescript-eslint: 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) - '@sveltejs/kit@2.47.2(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0))': + '@sveltejs/kit@2.48.3(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)) + '@sveltejs/vite-plugin-svelte': 3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 - devalue: 5.4.1 + devalue: 5.4.2 esm-env: 1.2.2 kleur: 4.1.5 - magic-string: 0.30.19 + magic-string: 0.30.21 mrmime: 2.0.1 sade: 1.8.1 - set-cookie-parser: 2.7.1 + set-cookie-parser: 2.7.2 sirv: 3.0.2 svelte: 4.2.20 - vite: 5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0) + vite: 5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0) '@sveltejs/package@2.5.4(svelte@4.2.20)(typescript@5.9.3)': dependencies: @@ -4555,153 +4578,153 @@ snapshots: transitivePeerDependencies: - typescript - '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0))': + '@sveltejs/vite-plugin-svelte-inspector@2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)) + '@sveltejs/vite-plugin-svelte': 3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)) debug: 4.4.3(supports-color@5.5.0) svelte: 4.2.20 - vite: 5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0) + vite: 5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.41.1)(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@5.41.1)(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.43.2)(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@5.43.2)(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.41.1)(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.43.2)(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) debug: 4.4.3(supports-color@5.5.0) - svelte: 5.41.1 - vite: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) + svelte: 5.43.2 + vite: 6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0))': + '@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)) + '@sveltejs/vite-plugin-svelte-inspector': 2.1.0(@sveltejs/vite-plugin-svelte@3.1.2(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@4.2.20)(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)) debug: 4.4.3(supports-color@5.5.0) deepmerge: 4.3.1 kleur: 4.1.5 - magic-string: 0.30.19 + magic-string: 0.30.21 svelte: 4.2.20 svelte-hmr: 0.16.0(svelte@4.2.20) - vite: 5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0) - vitefu: 0.2.5(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)) + vite: 5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0) + vitefu: 0.2.5(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.41.1)(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))': + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.43.2)(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.41.1)(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@5.41.1)(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.43.2)(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)))(svelte@5.43.2)(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) debug: 4.4.3(supports-color@5.5.0) deepmerge: 4.3.1 kleur: 4.1.5 - magic-string: 0.30.19 - svelte: 5.41.1 - vite: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) - vitefu: 1.1.1(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) + magic-string: 0.30.21 + svelte: 5.43.2 + vite: 6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) + vitefu: 1.1.1(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)) transitivePeerDependencies: - supports-color - '@tailwindcss/node@4.1.15': + '@tailwindcss/node@4.1.16': dependencies: '@jridgewell/remapping': 2.3.5 enhanced-resolve: 5.18.3 jiti: 2.6.1 lightningcss: 1.30.2 - magic-string: 0.30.19 + magic-string: 0.30.21 source-map-js: 1.2.1 - tailwindcss: 4.1.15 + tailwindcss: 4.1.16 - '@tailwindcss/oxide-android-arm64@4.1.15': + '@tailwindcss/oxide-android-arm64@4.1.16': optional: true - '@tailwindcss/oxide-darwin-arm64@4.1.15': + '@tailwindcss/oxide-darwin-arm64@4.1.16': optional: true - '@tailwindcss/oxide-darwin-x64@4.1.15': + '@tailwindcss/oxide-darwin-x64@4.1.16': optional: true - '@tailwindcss/oxide-freebsd-x64@4.1.15': + '@tailwindcss/oxide-freebsd-x64@4.1.16': optional: true - '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.15': + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16': optional: true - '@tailwindcss/oxide-linux-arm64-gnu@4.1.15': + '@tailwindcss/oxide-linux-arm64-gnu@4.1.16': optional: true - '@tailwindcss/oxide-linux-arm64-musl@4.1.15': + '@tailwindcss/oxide-linux-arm64-musl@4.1.16': optional: true - '@tailwindcss/oxide-linux-x64-gnu@4.1.15': + '@tailwindcss/oxide-linux-x64-gnu@4.1.16': optional: true - '@tailwindcss/oxide-linux-x64-musl@4.1.15': + '@tailwindcss/oxide-linux-x64-musl@4.1.16': optional: true - '@tailwindcss/oxide-wasm32-wasi@4.1.15': + '@tailwindcss/oxide-wasm32-wasi@4.1.16': optional: true - '@tailwindcss/oxide-win32-arm64-msvc@4.1.15': + '@tailwindcss/oxide-win32-arm64-msvc@4.1.16': optional: true - '@tailwindcss/oxide-win32-x64-msvc@4.1.15': + '@tailwindcss/oxide-win32-x64-msvc@4.1.16': optional: true - '@tailwindcss/oxide@4.1.15': + '@tailwindcss/oxide@4.1.16': optionalDependencies: - '@tailwindcss/oxide-android-arm64': 4.1.15 - '@tailwindcss/oxide-darwin-arm64': 4.1.15 - '@tailwindcss/oxide-darwin-x64': 4.1.15 - '@tailwindcss/oxide-freebsd-x64': 4.1.15 - '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.15 - '@tailwindcss/oxide-linux-arm64-gnu': 4.1.15 - '@tailwindcss/oxide-linux-arm64-musl': 4.1.15 - '@tailwindcss/oxide-linux-x64-gnu': 4.1.15 - '@tailwindcss/oxide-linux-x64-musl': 4.1.15 - '@tailwindcss/oxide-wasm32-wasi': 4.1.15 - '@tailwindcss/oxide-win32-arm64-msvc': 4.1.15 - '@tailwindcss/oxide-win32-x64-msvc': 4.1.15 - - '@tailwindcss/typography@0.5.19(tailwindcss@4.1.15)': + '@tailwindcss/oxide-android-arm64': 4.1.16 + '@tailwindcss/oxide-darwin-arm64': 4.1.16 + '@tailwindcss/oxide-darwin-x64': 4.1.16 + '@tailwindcss/oxide-freebsd-x64': 4.1.16 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.16 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.16 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.16 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.16 + '@tailwindcss/oxide-linux-x64-musl': 4.1.16 + '@tailwindcss/oxide-wasm32-wasi': 4.1.16 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.16 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.16 + + '@tailwindcss/typography@0.5.19(tailwindcss@4.1.16)': dependencies: postcss-selector-parser: 6.0.10 - tailwindcss: 4.1.15 + tailwindcss: 4.1.16 - '@tailwindcss/vite@4.1.15(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0))': + '@tailwindcss/vite@4.1.16(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0))': dependencies: - '@tailwindcss/node': 4.1.15 - '@tailwindcss/oxide': 4.1.15 - tailwindcss: 4.1.15 - vite: 5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0) + '@tailwindcss/node': 4.1.16 + '@tailwindcss/oxide': 4.1.16 + tailwindcss: 4.1.16 + vite: 5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0) - '@tailwindcss/vite@4.1.15(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))': + '@tailwindcss/vite@4.1.16(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))': dependencies: - '@tailwindcss/node': 4.1.15 - '@tailwindcss/oxide': 4.1.15 - tailwindcss: 4.1.15 - vite: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) + '@tailwindcss/node': 4.1.16 + '@tailwindcss/oxide': 4.1.16 + tailwindcss: 4.1.16 + vite: 6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) '@tsconfig/svelte@5.0.5': {} '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.28.4 - '@babel/types': 7.28.4 + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.28.4 + '@babel/types': 7.28.5 '@types/cookie@0.6.0': {} @@ -4733,11 +4756,11 @@ snapshots: dependencies: undici-types: 5.26.5 - '@types/node@22.18.12': + '@types/node@22.18.13': dependencies: undici-types: 6.21.0 - '@types/node@24.9.1': + '@types/node@24.9.2': dependencies: undici-types: 7.16.0 @@ -4757,7 +4780,7 @@ snapshots: '@typescript-eslint/eslint-plugin@8.46.2(@typescript-eslint/parser@8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@eslint-community/regexpp': 4.12.1 + '@eslint-community/regexpp': 4.12.2 '@typescript-eslint/parser': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.46.2 '@typescript-eslint/type-utils': 8.46.2(eslint@9.38.0(jiti@2.6.1))(typescript@5.9.3) @@ -4848,21 +4871,21 @@ snapshots: '@typescript-eslint/types': 8.46.2 eslint-visitor-keys: 4.2.1 - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))': dependencies: - '@babel/core': 7.28.4 - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.4) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.4) + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5) '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) + vite: 6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))(vue@3.5.22(typescript@5.9.3))': + '@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0))(vue@3.5.22(typescript@5.9.3))': dependencies: - vite: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) + vite: 6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) vue: 3.5.22(typescript@5.9.3) '@volar/language-core@2.4.15': @@ -4879,7 +4902,7 @@ snapshots: '@vue/compiler-core@3.5.22': dependencies: - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@vue/shared': 3.5.22 entities: 4.5.0 estree-walker: 2.0.2 @@ -4892,13 +4915,13 @@ snapshots: '@vue/compiler-sfc@3.5.22': dependencies: - '@babel/parser': 7.28.4 + '@babel/parser': 7.28.5 '@vue/compiler-core': 3.5.22 '@vue/compiler-dom': 3.5.22 '@vue/compiler-ssr': 3.5.22 '@vue/shared': 3.5.22 estree-walker: 2.0.2 - magic-string: 0.30.19 + magic-string: 0.30.21 postcss: 8.5.6 source-map-js: 1.2.1 @@ -5196,7 +5219,7 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axios@1.12.2: + axios@1.13.1: dependencies: follow-redirects: 1.15.11 form-data: 4.0.4 @@ -5208,7 +5231,7 @@ snapshots: balanced-match@1.0.2: {} - baseline-browser-mapping@2.8.18: {} + baseline-browser-mapping@2.8.21: {} big.js@5.2.2: {} @@ -5246,13 +5269,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.26.3: + browserslist@4.27.0: dependencies: - baseline-browser-mapping: 2.8.18 + baseline-browser-mapping: 2.8.21 caniuse-lite: 1.0.30001751 - electron-to-chromium: 1.5.237 - node-releases: 2.0.26 - update-browserslist-db: 1.1.3(browserslist@4.26.3) + electron-to-chromium: 1.5.243 + node-releases: 2.0.27 + update-browserslist-db: 1.1.4(browserslist@4.27.0) buffer-from@1.1.2: {} @@ -5465,7 +5488,7 @@ snapshots: detect-libc@2.1.2: {} - devalue@5.4.1: {} + devalue@5.4.2: {} doctrine@2.1.0: dependencies: @@ -5483,7 +5506,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.237: {} + electron-to-chromium@1.5.243: {} emoji-regex@8.0.0: {} @@ -5568,7 +5591,7 @@ snapshots: dependencies: acorn: 8.15.0 acorn-walk: 8.3.4 - browserslist: 4.26.3 + browserslist: 4.27.0 commander: 14.0.1 fast-brake: 0.1.6 fast-glob: 3.3.3 @@ -5703,7 +5726,7 @@ snapshots: eslint-plugin-es-x@7.8.0(eslint@9.38.0(jiti@2.6.1)): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1)) - '@eslint-community/regexpp': 4.12.1 + '@eslint-community/regexpp': 4.12.2 eslint: 9.38.0(jiti@2.6.1) eslint-compat-utils: 0.5.1(eslint@9.38.0(jiti@2.6.1)) @@ -5713,7 +5736,7 @@ snapshots: enhanced-resolve: 5.18.3 eslint: 9.38.0(jiti@2.6.1) eslint-plugin-es-x: 7.8.0(eslint@9.38.0(jiti@2.6.1)) - get-tsconfig: 4.12.0 + get-tsconfig: 4.13.0 globals: 15.15.0 globrex: 0.1.2 ignore: 5.3.2 @@ -5748,7 +5771,7 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-svelte@3.12.5(eslint@9.38.0(jiti@2.6.1))(svelte@4.2.20): + eslint-plugin-svelte@3.13.0(eslint@9.38.0(jiti@2.6.1))(svelte@4.2.20): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1)) '@jridgewell/sourcemap-codec': 1.5.5 @@ -5849,13 +5872,13 @@ snapshots: eslint@9.38.0(jiti@2.6.1): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.38.0(jiti@2.6.1)) - '@eslint-community/regexpp': 4.12.1 + '@eslint-community/regexpp': 4.12.2 '@eslint/config-array': 0.21.1 - '@eslint/config-helpers': 0.4.1 + '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.16.0 '@eslint/eslintrc': 3.3.1 '@eslint/js': 9.38.0 - '@eslint/plugin-kit': 0.4.0 + '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 @@ -5907,7 +5930,7 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.1.0: + esrap@2.1.1: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6108,7 +6131,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 - get-tsconfig@4.12.0: + get-tsconfig@4.13.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -6375,7 +6398,7 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 24.9.1 + '@types/node': 24.9.2 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -6423,16 +6446,23 @@ snapshots: kuler@2.0.0: {} - laravel-vite-plugin@1.3.0(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)): + laravel-precognition@0.7.3: + dependencies: + axios: 1.13.1 + lodash-es: 4.17.21 + transitivePeerDependencies: + - debug + + laravel-vite-plugin@1.3.0(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)): dependencies: picocolors: 1.1.1 - vite: 5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0) + vite: 5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0) vite-plugin-full-reload: 1.2.0 - laravel-vite-plugin@1.3.0(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)): + laravel-vite-plugin@1.3.0(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)): dependencies: picocolors: 1.1.1 - vite: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) + vite: 6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) vite-plugin-full-reload: 1.2.0 levn@0.4.1: @@ -6532,7 +6562,7 @@ snapshots: dependencies: yallist: 3.1.1 - magic-string@0.30.19: + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6613,7 +6643,7 @@ snapshots: neo-async@2.6.2: {} - node-releases@2.0.26: {} + node-releases@2.0.27: {} nodemon@3.1.10: dependencies: @@ -6826,17 +6856,17 @@ snapshots: optionalDependencies: vue-tsc: 2.2.12(typescript@5.9.3) - prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.41.1): + prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.43.2): dependencies: prettier: 3.6.2 - svelte: 5.41.1 + svelte: 5.43.2 - prettier-plugin-tailwindcss@0.6.14(prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3)))(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.41.1))(prettier@3.6.2): + prettier-plugin-tailwindcss@0.6.14(prettier-plugin-organize-imports@4.3.0(prettier@3.6.2)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3)))(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.43.2))(prettier@3.6.2): dependencies: prettier: 3.6.2 optionalDependencies: prettier-plugin-organize-imports: 4.3.0(prettier@3.6.2)(typescript@5.9.3)(vue-tsc@2.2.12(typescript@5.9.3)) - prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@5.41.1) + prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@5.43.2) prettier@3.6.2: {} @@ -7064,7 +7094,7 @@ snapshots: transitivePeerDependencies: - supports-color - set-cookie-parser@2.7.1: {} + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: dependencies: @@ -7250,14 +7280,14 @@ snapshots: transitivePeerDependencies: - picomatch - svelte-check@4.3.3(picomatch@4.0.3)(svelte@5.41.1)(typescript@5.9.3): + svelte-check@4.3.3(picomatch@4.0.3)(svelte@5.43.2)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.41.1 + svelte: 5.43.2 typescript: 5.9.3 transitivePeerDependencies: - picomatch @@ -7298,10 +7328,10 @@ snapshots: estree-walker: 3.0.3 is-reference: 3.0.3 locate-character: 3.0.0 - magic-string: 0.30.19 + magic-string: 0.30.21 periscopic: 3.1.0 - svelte@5.41.1: + svelte@5.43.2: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -7312,10 +7342,10 @@ snapshots: axobject-query: 4.1.0 clsx: 2.1.1 esm-env: 1.2.2 - esrap: 2.1.0 + esrap: 2.1.1 is-reference: 3.0.3 locate-character: 3.0.0 - magic-string: 0.30.19 + magic-string: 0.30.21 zimmerframe: 1.1.4 table@6.9.0: @@ -7326,7 +7356,7 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 - tailwindcss@4.1.15: {} + tailwindcss@4.1.16: {} tapable@2.3.0: {} @@ -7456,9 +7486,9 @@ snapshots: unpipe@1.0.0: {} - update-browserslist-db@1.1.3(browserslist@4.26.3): + update-browserslist-db@1.1.4(browserslist@4.27.0): dependencies: - browserslist: 4.26.3 + browserslist: 4.27.0 escalade: 3.2.0 picocolors: 1.1.1 @@ -7479,18 +7509,18 @@ snapshots: picocolors: 1.1.1 picomatch: 2.3.1 - vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0): + vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0): dependencies: esbuild: 0.21.5 postcss: 8.5.6 rollup: 4.52.5 optionalDependencies: - '@types/node': 24.9.1 + '@types/node': 24.9.2 fsevents: 2.3.3 lightningcss: 1.30.2 terser: 5.44.0 - vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0): + vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0): dependencies: esbuild: 0.25.11 fdir: 6.5.0(picomatch@4.0.3) @@ -7499,19 +7529,19 @@ snapshots: rollup: 4.52.5 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 24.9.1 + '@types/node': 24.9.2 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 terser: 5.44.0 - vitefu@0.2.5(vite@5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0)): + vitefu@0.2.5(vite@5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0)): optionalDependencies: - vite: 5.4.21(@types/node@24.9.1)(lightningcss@1.30.2)(terser@5.44.0) + vite: 5.4.21(@types/node@24.9.2)(lightningcss@1.30.2)(terser@5.44.0) - vitefu@1.1.1(vite@6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)): + vitefu@1.1.1(vite@6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)): optionalDependencies: - vite: 6.4.1(@types/node@24.9.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) + vite: 6.4.1(@types/node@24.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0) vscode-uri@3.1.0: {} @@ -7560,7 +7590,7 @@ snapshots: '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.26.3 + browserslist: 4.27.0 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 diff --git a/tests/app/server.js b/tests/app/server.js index f9238a104..757084e44 100644 --- a/tests/app/server.js +++ b/tests/app/server.js @@ -12,6 +12,7 @@ app.use(bodyParser.json({ extended: true })) const upload = multer() const adapters = ['react', 'svelte', 'vue3'] +const runsInCI = !!process.env.CI if (!adapters.includes(inertia.package)) { throw new Error(`Invalid adapter package "${inertia.package}". Expected one of: ${adapters.join(', ')}.`) @@ -952,6 +953,162 @@ app.get('/form-component/invalidate-tags/:propType', (req, res) => }), ) +// + +app.post('/form-component/precognition', (req, res) => { + setTimeout( + () => { + const only = req.headers['precognition-validate-only'] ? req.headers['precognition-validate-only'].split(',') : [] + const name = req.body['name'] + const email = req.body['email'] + const errors = {} + + if (!name) { + errors.name = 'The name field is required.' + } + + if (name && name.length < 3) { + errors.name = 'The name must be at least 3 characters.' + } + + if (!email) { + errors.email = 'The email field is required.' + } + + if (email && !/\S+@\S+\.\S+/.test(email)) { + errors.email = 'The email must be a valid email address.' + } + + if (only.length) { + Object.keys(errors).forEach((key) => { + if (!only.includes(key)) { + delete errors[key] + } + }) + } + + res.header('Precognition', 'true') + res.header('Vary', 'Precognition') + + if (Object.keys(errors).length) { + return res.status(422).json({ errors }) + } + + return res.status(204).header('Precognition-Success', 'true').send() + }, + !!req.query['slow'] ? 2000 : 250, + ) +}) + +app.post('/form-component/precognition-array-errors', (req, res) => { + setTimeout(() => { + const only = req.headers['precognition-validate-only'] ? req.headers['precognition-validate-only'].split(',') : [] + const name = req.body['name'] + const email = req.body['email'] + const errors = {} + + if (!name) { + errors.name = ['The name field is required.'] + } + + if (name && name.length < 3) { + errors.name = ['The name must be at least 3 characters.', 'The name contains invalid characters.'] + } + + if (!email) { + errors.email = ['The email field is required.'] + } + + if (email && !/\S+@\S+\.\S+/.test(email)) { + errors.email = ['The email must be a valid email address.', 'The email format is incorrect.'] + } + + if (only.length) { + Object.keys(errors).forEach((key) => { + if (!only.includes(key)) { + delete errors[key] + } + }) + } + + res.header('Precognition', 'true') + res.header('Vary', 'Precognition') + + if (Object.keys(errors).length) { + return res.status(422).json({ errors }) + } + + return res.status(204).header('Precognition-Success', 'true').send() + }, 250) +}) + +app.post('/form-component/precognition-files', upload.any(), (req, res) => { + setTimeout(() => { + console.log(req, req) + const only = req.headers['precognition-validate-only'] ? req.headers['precognition-validate-only'].split(',') : [] + const name = req.body['name'] + const hasAvatar = req.files && req.files.avatar + const errors = {} + + if (!name) { + errors.name = 'The name field is required.' + } + + if (name && name.length < 3) { + errors.name = 'The name must be at least 3 characters.' + } + + if (!hasAvatar) { + errors.avatar = 'The avatar field is required.' + } + + if (only.length) { + Object.keys(errors).forEach((key) => { + if (!only.includes(key)) { + delete errors[key] + } + }) + } + + res.header('Precognition', 'true') + res.header('Vary', 'Precognition') + + if (Object.keys(errors).length) { + return res.status(422).json({ errors }) + } + + return res.status(204).header('Precognition-Success', 'true').send() + }, 250) +}) + +app.post('/form-component/precognition-headers', (req, res) => { + setTimeout(() => { + const customHeader = req.headers['x-custom-header'] + const name = req.body['name'] + const errors = {} + + // Show error when custom header IS present (to prove it was sent) + if (customHeader === 'custom-value') { + errors.name = 'Custom header received: custom-value' + } else if (!name) { + errors.name = 'The name field is required.' + } else if (name.length < 3) { + errors.name = 'The name must be at least 3 characters.' + } + + res.header('Precognition', 'true') + res.header('Vary', 'Precognition') + + if (Object.keys(errors).length) { + return res.status(422).json({ errors }) + } + + return res.status(204).header('Precognition-Success', 'true').send() + }, 250) +}) + +// + function renderInfiniteScroll(req, res, component, total = 40, orderByDesc = false, perPage = 15) { const page = req.query.page ? parseInt(req.query.page) : 1 const partialReload = !!req.headers['x-inertia-partial-data'] diff --git a/tests/form-component.spec.ts b/tests/form-component.spec.ts index 95ec296a2..94ac7ebc9 100644 --- a/tests/form-component.spec.ts +++ b/tests/form-component.spec.ts @@ -1431,6 +1431,534 @@ test.describe('Form Component', () => { }) }) + test.describe('Precognition', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/form-component/precognition') + }) + + test('does not validate when field is untouched', async ({ page }) => { + await page.locator('input[name="name"]').focus() + await page.waitForTimeout(100) + await page.locator('input[name="name"]').blur() + + for (let i = 0; i < 5; i++) { + await expect(page.getByText('Validating...')).not.toBeVisible() + await page.waitForTimeout(50) + } + + await expect(page.getByText('The name field is required.')).not.toBeVisible() + }) + + test('shows validation error when field is invalid', async ({ page }) => { + await page.fill('input[name="name"]', 'ab') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + }) + + test('clears validation error when field becomes valid', async ({ page }) => { + await page.fill('input[name="name"]', 'ab') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + + await page.fill('input[name="name"]', 'John Doe') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('The name must be at least 3 characters.')).not.toBeVisible() + }) + + test('validates only the specified field', async ({ page }) => { + await page.fill('input[name="name"]', 'ab') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + await expect(page.getByText('The email field is required.')).not.toBeVisible() + }) + + test('validates multiple fields independently', async ({ page }) => { + await page.fill('input[name="name"]', 'ab') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + await expect(page.getByText('The email must be a valid email address.')).not.toBeVisible() + + await page.fill('input[name="email"]', 'x') + await page.locator('input[name="email"]').blur() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + await expect(page.getByText('The email must be a valid email address.')).toBeVisible() + }) + + test('does not clear unrelated field errors', async ({ page }) => { + await page.fill('input[name="name"]', 'ab') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + + await page.fill('input[name="email"]', 'x') + await page.locator('input[name="email"]').blur() + + await expect(page.getByText('The email must be a valid email address.')).toBeVisible() + + await page.fill('input[name="email"]', 'test@example.com') + await page.locator('input[name="email"]').blur() + + await expect(page.getByText('The email must be a valid email address.')).not.toBeVisible() + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + }) + + test('field is valid when validated and no errors exist', async ({ page }) => { + await expect(page.getByText('Name is valid!')).not.toBeVisible() + + await page.fill('input[name="name"]', 'John Doe') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('Name is valid!')).toBeVisible() + }) + + test('field is not valid before validation', async ({ page }) => { + await expect(page.getByText('Name is valid!')).not.toBeVisible() + + await page.fill('input[name="name"]', 'John Doe') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('Name is valid!')).toBeVisible() + await expect(page.getByText('The name must be at least 3 characters.')).not.toBeVisible() + }) + + test('field is not valid after failed validation', async ({ page }) => { + await page.fill('input[name="name"]', 'ab') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + await expect(page.getByText('Name is valid!')).not.toBeVisible() + }) + + test('valid field persists after successful validation', async ({ page }) => { + await page.fill('input[name="name"]', 'John Doe') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('Name is valid!')).toBeVisible() + + await page.fill('input[name="name"]', 'Jane Doe') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('Name is valid!')).toBeVisible() + }) + + test('valid field becomes invalid when field is revalidated with errors', async ({ page }) => { + await page.fill('input[name="name"]', 'John Doe') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('Name is valid!')).toBeVisible() + + await page.fill('input[name="name"]', 'ab') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + await expect(page.getByText('Name is valid!')).not.toBeVisible() + }) + + test('shows only first error when server returns errors as array', async ({ page }) => { + await page.goto('/form-component/precognition-array-errors') + + await page.fill('input[name="name"]', 'ab') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + // Should show only the first error from the array, not the second + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + await expect(page.getByText('The name contains invalid characters.')).not.toBeVisible() + }) + + test('shows all errors when simpleValidationErrors is false', async ({ page }) => { + await page.goto('/form-component/precognition-all-errors') + + await page.fill('input[name="name"]', 'ab') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + // Should show all errors from the array + await expect(page.locator('#name-error-0')).toHaveText('The name must be at least 3 characters.') + await expect(page.locator('#name-error-1')).toHaveText('The name contains invalid characters.') + }) + + test('validates all touched fields when calling validate() without arguments', async ({ page }) => { + await page.goto('/form-component/precognition-methods') + + await page.fill('input[name="name"]', 'ab') + await page.locator('input[name="name"]').blur() + + await page.fill('input[name="email"]', 'x') + await page.locator('input[name="email"]').blur() + + await expect(page.getByText('Validating...')).not.toBeVisible() + await expect(page.getByText('The name must be at least 3 characters.')).not.toBeVisible() + await expect(page.getByText('The email must be a valid email address.')).not.toBeVisible() + + await page.getByRole('button', { name: 'Validate All Touched' }).click() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + await expect(page.getByText('The email must be a valid email address.')).toBeVisible() + }) + + test('reset all fields clears all touched fields', async ({ page }) => { + await page.goto('/form-component/precognition-methods') + + await page.fill('input[name="name"]', 'ab') + await page.locator('input[name="name"]').blur() + + await page.fill('input[name="email"]', 'x') + await page.locator('input[name="email"]').blur() + + await page.getByRole('button', { name: 'Reset All' }).click() + + await expect(page.locator('input[name="name"]')).toHaveValue('') + await expect(page.locator('input[name="email"]')).toHaveValue('') + + await page.fill('input[name="name"]', 'ab') + await page.locator('input[name="name"]').blur() + + await page.getByRole('button', { name: 'Validate All Touched' }).click() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + await expect(page.getByText('The email field is required.')).not.toBeVisible() + }) + + test('reset specific fields removes only those fields from touched', async ({ page }) => { + await page.goto('/form-component/precognition-methods') + + await page.fill('input[name="name"]', 'ab') + await page.locator('input[name="name"]').blur() + + await page.fill('input[name="email"]', 'x') + await page.locator('input[name="email"]').blur() + + await expect(page.locator('input[name="name"]')).toHaveValue('ab') + await expect(page.locator('input[name="email"]')).toHaveValue('x') + + await page.getByRole('button', { name: 'Reset Name', exact: true }).click() + + await expect(page.locator('input[name="name"]')).toHaveValue('') + await expect(page.locator('input[name="email"]')).toHaveValue('x') + + await page.fill('input[name="email"]', 'y') + + await page.getByRole('button', { name: 'Validate All Touched' }).click() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('The name field is required.')).not.toBeVisible() + await expect(page.getByText('The email must be a valid email address.')).toBeVisible() + }) + + test('touch with array marks multiple fields as touched', async ({ page }) => { + await page.goto('/form-component/precognition-methods') + + await page.getByRole('button', { name: 'Touch Name and Email' }).click() + await page.getByRole('button', { name: 'Validate All Touched' }).click() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('The name field is required.')).toBeVisible() + await expect(page.getByText('The email field is required.')).toBeVisible() + }) + + test('touch deduplicates fields when called multiple times', async ({ page }) => { + await page.goto('/form-component/precognition-methods') + + await page.fill('input[name="name"]', 'ab') + + await page.getByRole('button', { name: 'Touch Name Twice' }).click() + await page.getByRole('button', { name: 'Validate All Touched' }).click() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + await expect(page.getByText('The email must be a valid email address.')).not.toBeVisible() + }) + + test('touched() returns false when no fields are touched', async ({ page }) => { + await page.goto('/form-component/precognition-methods') + + await expect(page.locator('#any-touched')).toHaveText('Form has no touched fields') + await expect(page.locator('#name-touched')).toHaveText('Name is not touched') + await expect(page.locator('#email-touched')).toHaveText('Email is not touched') + }) + + test('touched(field) returns true when specific field is touched', async ({ page }) => { + await page.goto('/form-component/precognition-methods') + + await page.locator('input[name="name"]').focus() + await page.locator('input[name="name"]').blur() + + await expect(page.locator('#name-touched')).toHaveText('Name is touched') + await expect(page.locator('#email-touched')).toHaveText('Email is not touched') + await expect(page.locator('#any-touched')).toHaveText('Form has touched fields') + }) + + test('touched() returns true when any field is touched', async ({ page }) => { + await page.goto('/form-component/precognition-methods') + + await page.locator('input[name="email"]').focus() + await page.locator('input[name="email"]').blur() + + await expect(page.locator('#any-touched')).toHaveText('Form has touched fields') + await expect(page.locator('#email-touched')).toHaveText('Email is touched') + await expect(page.locator('#name-touched')).toHaveText('Name is not touched') + }) + + test('touched() updates when multiple fields are touched', async ({ page }) => { + await page.goto('/form-component/precognition-methods') + + await page.locator('input[name="name"]').focus() + await page.locator('input[name="name"]').blur() + await page.locator('input[name="email"]').focus() + await page.locator('input[name="email"]').blur() + + await expect(page.locator('#name-touched')).toHaveText('Name is touched') + await expect(page.locator('#email-touched')).toHaveText('Email is touched') + await expect(page.locator('#any-touched')).toHaveText('Form has touched fields') + }) + + test('validating a specific field also validates previously touched inputs', async ({ page }) => { + await page.goto('/form-component/precognition-methods') + + await page.fill('input[name="name"]', 'ab') + await page.fill('input[name="email"]', 'x') + + await page.getByRole('button', { name: 'Validate Name', exact: true }).click() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + await expect(page.getByText('The email must be a valid email address.')).toBeVisible() + }) + + test('validate with array of fields validates multiple fields', async ({ page }) => { + await page.goto('/form-component/precognition-methods') + + await page.getByRole('button', { name: 'Validate Name and Email' }).click() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('The name field is required.')).toBeVisible() + await expect(page.getByText('The email field is required.')).toBeVisible() + }) + + test('reset with array removes multiple fields from touched', async ({ page }) => { + await page.goto('/form-component/precognition-methods') + + await page.fill('input[name="name"]', 'ab') + await page.locator('input[name="name"]').blur() + + await page.fill('input[name="email"]', 'x') + await page.locator('input[name="email"]').blur() + + await page.getByRole('button', { name: 'Validate All Touched' }).click() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + await expect(page.getByText('The email must be a valid email address.')).toBeVisible() + + await page.getByRole('button', { name: 'Reset Name and Email' }).click() + + await expect(page.locator('input[name="name"]')).toHaveValue('') + await expect(page.locator('input[name="email"]')).toHaveValue('') + + await expect(page.getByText('Name is not touched')).toBeVisible() + await expect(page.getByText('Email is not touched')).toBeVisible() + await expect(page.getByText('Form has no touched fields')).toBeVisible() + + await page.fill('input[name="name"]', 'abc') + await page.fill('input[name="email"]', 'test@example.com') + + await page.getByRole('button', { name: 'Validate All Touched' }).click() + + await page.waitForTimeout(500) + + await expect(page.getByText('The name must be at least 3 characters.')).not.toBeVisible() + await expect(page.getByText('The email must be a valid email address.')).not.toBeVisible() + }) + + test('does not submit files by default', async ({ page }) => { + await page.goto('/form-component/precognition-files') + + await page.setInputFiles('#avatar', { + name: 'avatar.jpg', + mimeType: 'image/jpeg', + buffer: Buffer.from('fake image data'), + }) + + await page.getByRole('button', { name: 'Validate Both' }).click() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('The name field is required.')).toBeVisible() + await expect(page.getByText('The avatar field is required.')).toBeVisible() + }) + + test('validates files when validate-files prop is true', async ({ page }) => { + await page.goto('/form-component/precognition-files') + await page.getByRole('button', { name: /Toggle Validate Files/ }).click() + await expect(page.getByText('Toggle Validate Files (enabled)')).toBeVisible() + + await page.fill('input[name="name"]', 'ab') + await page.setInputFiles('#avatar', { + name: 'avatar.jpg', + mimeType: 'image/jpeg', + buffer: Buffer.from('fake image data'), + }) + + await page.getByRole('button', { name: 'Validate Both' }).click() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + await expect(page.getByText('The avatar field is required.')).not.toBeVisible() + }) + + test('transforms data for validation requests', async ({ page }) => { + await page.goto('/form-component/precognition-transform') + + await page.fill('input[name="name"]', 'a') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + + await page.fill('input[name="name"]', 'aa') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('The name must be at least 3 characters.')).not.toBeVisible() + await expect(page.getByText('Name is valid!')).toBeVisible() + }) + + test('calls onPrecognitionSuccess and onFinish callbacks when validation succeeds', async ({ page }) => { + await page.goto('/form-component/precognition-callbacks') + + await page.fill('input[name="name"]', 'John Doe') + await page.click('button:has-text("Validate")') + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('onPrecognitionSuccess called!')).toBeVisible() + await expect(page.getByText('onValidationError called!')).not.toBeVisible() + await expect(page.getByText('onFinish called!')).toBeVisible() + }) + + test('calls onValidationError and onFinish callbacks when validation fails', async ({ page }) => { + await page.goto('/form-component/precognition-callbacks') + + await page.fill('input[name="name"]', 'ab') + await page.click('button:has-text("Validate")') + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + await expect(page.getByText('onPrecognitionSuccess called!')).not.toBeVisible() + await expect(page.getByText('onValidationError called!')).toBeVisible() + await expect(page.getByText('onFinish called!')).toBeVisible() + }) + + test('onBefore can block validation', async ({ page }) => { + await page.goto('/form-component/precognition-before-validation') + + await page.fill('input[name="name"]', 'block') + await page.locator('input[name="name"]').blur() + + for (let i = 0; i < 5; i++) { + await expect(page.getByText('Validating...')).not.toBeVisible() + await page.waitForTimeout(50) + } + }) + + test('sends custom headers with validation requests', async ({ page }) => { + await page.goto('/form-component/precognition-headers') + + // Fill in a valid name to trigger validation + await page.fill('input[name="name"]', 'John Doe') + await page.locator('input[name="name"]').blur() + + await expect(page.getByText('Validating...')).toBeVisible() + await expect(page.getByText('Validating...')).not.toBeVisible() + + // Should show error confirming custom header was received + await expect(page.getByText('Custom header received: custom-value')).toBeVisible() + }) + + test('automatically cancels previous validation when new validation starts', async ({ page }) => { + await page.goto('/form-component/precognition-cancel') + + requests.listenForFailed(page) + requests.listenForResponses(page) + + await page.fill('#auto-cancel-name-input', 'ab') + await page.locator('#auto-cancel-name-input').blur() + await expect(page.getByText('Validating...')).toBeVisible() + + // Immediately change value and trigger new validation - should cancel the first one + await page.fill('#auto-cancel-name-input', 'xy') + await page.locator('#auto-cancel-name-input').blur() + await expect(page.getByText('Validating...')).not.toBeVisible() + await expect(page.getByText('The name must be at least 3 characters.')).toBeVisible() + + // One cancelled, one 422 response + expect(requests.failed).toHaveLength(1) + expect(requests.responses).toHaveLength(1) + + const cancelledRequestError = await requests.failed[0].failure()?.errorText + expect(cancelledRequestError).toBe('net::ERR_ABORTED') + }) + }) + test.describe('React', () => { test.skip(process.env.PACKAGE !== 'react', 'Skipping React-specific tests') diff --git a/tests/support.ts b/tests/support.ts index bae7d4abb..a3d2e0009 100644 --- a/tests/support.ts +++ b/tests/support.ts @@ -1,4 +1,4 @@ -import { expect, Page, Request } from '@playwright/test' +import { expect, Page, Request, Response } from '@playwright/test' export const clickAndWaitForResponse = async ( page: Page, @@ -43,6 +43,8 @@ export const consoleMessages = { export const requests = { requests: [] as Request[], finished: [] as Request[], + failed: [] as Request[], + responses: [] as Response[], listen(page: Page) { this.requests = [] @@ -53,6 +55,16 @@ export const requests = { this.finished = [] page.on('requestfinished', (request) => this.finished.push(request)) }, + + listenForFailed(page: Page) { + this.failed = [] + page.on('requestfailed', (request) => this.failed.push(request)) + }, + + listenForResponses(page: Page) { + this.responses = [] + page.on('response', (data) => this.responses.push(data)) + }, } export const shouldBeDumpPage = async (page: Page, method: 'get' | 'post' | 'patch' | 'put' | 'delete') => {