From 4074dce402f0296157d8f358272f2fcd80babda6 Mon Sep 17 00:00:00 2001 From: "bert.hausmans" Date: Thu, 23 Apr 2026 17:20:59 +0200 Subject: [PATCH] feat(portal): public-form component architecture Replace monolithic register/[eventSlug].vue with composable field renderer, conditional-logic engine, stepper, and per-field components driven by Form Builder schema. Adds flatpickr for date fields. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/portal/auto-imports.d.ts | 59 +- apps/portal/components.d.ts | 19 + apps/portal/package.json | 6 + apps/portal/pnpm-lock.yaml | 812 +++++++++ .../components/public-form/FieldBoolean.vue | 40 + .../public-form/FieldCheckboxList.vue | 107 ++ .../src/components/public-form/FieldDate.vue | 37 + .../src/components/public-form/FieldEmail.vue | 38 + .../components/public-form/FieldHeading.vue | 22 + .../public-form/FieldMultiselect.vue | 67 + .../components/public-form/FieldNumber.vue | 54 + .../components/public-form/FieldParagraph.vue | 13 + .../src/components/public-form/FieldPhone.vue | 39 + .../src/components/public-form/FieldRadio.vue | 85 + .../components/public-form/FieldRenderer.vue | 208 +++ .../components/public-form/FieldSelect.vue | 65 + .../src/components/public-form/FieldText.vue | 35 + .../components/public-form/FieldTextarea.vue | 37 + .../src/components/public-form/FieldUrl.vue | 38 + .../public-form/FormConfirmation.vue | 114 ++ .../components/public-form/FormErrorState.vue | 118 ++ .../components/public-form/FormStepper.vue | 40 + .../public-form/SubmitterDetails.vue | 80 + .../src/composables/api/usePublicForm.ts | 110 ++ .../src/composables/useConditionalLogic.ts | 127 ++ apps/portal/src/composables/useFormSteps.ts | 138 ++ .../portal/src/pages/register/[eventSlug].vue | 1537 ----------------- apps/portal/src/utils/formValidation.ts | 149 ++ apps/portal/typed-router.d.ts | 2 +- 29 files changed, 2622 insertions(+), 1574 deletions(-) create mode 100644 apps/portal/src/components/public-form/FieldBoolean.vue create mode 100644 apps/portal/src/components/public-form/FieldCheckboxList.vue create mode 100644 apps/portal/src/components/public-form/FieldDate.vue create mode 100644 apps/portal/src/components/public-form/FieldEmail.vue create mode 100644 apps/portal/src/components/public-form/FieldHeading.vue create mode 100644 apps/portal/src/components/public-form/FieldMultiselect.vue create mode 100644 apps/portal/src/components/public-form/FieldNumber.vue create mode 100644 apps/portal/src/components/public-form/FieldParagraph.vue create mode 100644 apps/portal/src/components/public-form/FieldPhone.vue create mode 100644 apps/portal/src/components/public-form/FieldRadio.vue create mode 100644 apps/portal/src/components/public-form/FieldRenderer.vue create mode 100644 apps/portal/src/components/public-form/FieldSelect.vue create mode 100644 apps/portal/src/components/public-form/FieldText.vue create mode 100644 apps/portal/src/components/public-form/FieldTextarea.vue create mode 100644 apps/portal/src/components/public-form/FieldUrl.vue create mode 100644 apps/portal/src/components/public-form/FormConfirmation.vue create mode 100644 apps/portal/src/components/public-form/FormErrorState.vue create mode 100644 apps/portal/src/components/public-form/FormStepper.vue create mode 100644 apps/portal/src/components/public-form/SubmitterDetails.vue create mode 100644 apps/portal/src/composables/api/usePublicForm.ts create mode 100644 apps/portal/src/composables/useConditionalLogic.ts create mode 100644 apps/portal/src/composables/useFormSteps.ts delete mode 100644 apps/portal/src/pages/register/[eventSlug].vue create mode 100644 apps/portal/src/utils/formValidation.ts diff --git a/apps/portal/auto-imports.d.ts b/apps/portal/auto-imports.d.ts index 1a13a337..a4601439 100644 --- a/apps/portal/auto-imports.d.ts +++ b/apps/portal/auto-imports.d.ts @@ -46,10 +46,15 @@ declare global { const defineLoader: typeof import('vue-router/auto')['defineLoader'] const definePage: typeof import('unplugin-vue-router/runtime')['definePage'] const defineStore: typeof import('pinia')['defineStore'] + const draftIdempotencyKey: typeof import('./src/composables/useFormDraft')['draftIdempotencyKey'] + const draftSubmitterStorageKey: typeof import('./src/composables/useFormDraft')['draftSubmitterStorageKey'] const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] const effectScope: typeof import('vue')['effectScope'] const emailValidator: typeof import('./src/@core/utils/validators')['emailValidator'] + const evaluateConditionalLogic: typeof import('./src/composables/useConditionalLogic')['evaluateConditionalLogic'] const extendRef: typeof import('@vueuse/core')['extendRef'] + const extractErrorBody: typeof import('./src/composables/useFormDraft')['extractErrorBody'] + const extractRetryAfterSeconds: typeof import('./src/composables/useFormDraft')['extractRetryAfterSeconds'] const formatDate: typeof import('./src/@core/utils/formatters')['formatDate'] const formatDateToMonthShort: typeof import('./src/@core/utils/formatters')['formatDateToMonthShort'] const generateDeviceFingerprint: typeof import('./src/utils/deviceFingerprint')['generateDeviceFingerprint'] @@ -57,6 +62,7 @@ declare global { const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentScope: typeof import('vue')['getCurrentScope'] const getDeviceName: typeof import('./src/utils/deviceFingerprint')['getDeviceName'] + const getValidatorsForField: typeof import('./src/utils/formValidation')['getValidatorsForField'] const h: typeof import('vue')['h'] const hexToRgb: typeof import('./src/@core/utils/colorConverter')['hexToRgb'] const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] @@ -67,12 +73,14 @@ declare global { const isDefined: typeof import('@vueuse/core')['isDefined'] const isEmpty: typeof import('./src/@core/utils/helpers')['isEmpty'] const isEmptyArray: typeof import('./src/@core/utils/helpers')['isEmptyArray'] + const isFieldValueEmpty: typeof import('./src/utils/formValidation')['isFieldValueEmpty'] const isNullOrUndefined: typeof import('./src/@core/utils/helpers')['isNullOrUndefined'] const isObject: typeof import('./src/@core/utils/helpers')['isObject'] const isProxy: typeof import('vue')['isProxy'] const isReactive: typeof import('vue')['isReactive'] const isReadonly: typeof import('vue')['isReadonly'] const isRef: typeof import('vue')['isRef'] + const isStepValid: typeof import('./src/composables/useFormSteps')['isStepValid'] const isToday: typeof import('./src/@core/utils/helpers')['isToday'] const kFormatter: typeof import('./src/@core/utils/formatters')['kFormatter'] const lengthValidator: typeof import('./src/@core/utils/validators')['lengthValidator'] @@ -135,6 +143,7 @@ declare global { const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] const resolveVuetifyTheme: typeof import('./src/@core/utils/vuetify')['resolveVuetifyTheme'] const rgbaToHex: typeof import('./src/@core/utils/colorConverter')['rgbaToHex'] + const runValidators: typeof import('./src/utils/formValidation')['runValidators'] const setActivePinia: typeof import('pinia')['setActivePinia'] const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] const shallowReactive: typeof import('vue')['shallowReactive'] @@ -233,6 +242,8 @@ declare global { const useFloor: typeof import('@vueuse/math')['useFloor'] const useFocus: typeof import('@vueuse/core')['useFocus'] const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] + const useFormDraft: typeof import('./src/composables/useFormDraft')['useFormDraft'] + const useFormSteps: typeof import('./src/composables/useFormSteps')['useFormSteps'] const useFps: typeof import('@vueuse/core')['useFps'] const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] const useGamepad: typeof import('@vueuse/core')['useGamepad'] @@ -378,7 +389,6 @@ declare module 'vue' { interface GlobalComponents {} interface ComponentCustomProperties { readonly EffectScope: UnwrapRef - readonly acceptHMRUpdate: UnwrapRef readonly alphaDashValidator: UnwrapRef readonly alphaValidator: UnwrapRef readonly asyncComputed: UnwrapRef @@ -395,11 +405,8 @@ declare module 'vue' { readonly controlledRef: UnwrapRef readonly createApp: UnwrapRef readonly createEventHook: UnwrapRef - readonly createGenericProjection: UnwrapRef readonly createGlobalState: UnwrapRef readonly createInjectionState: UnwrapRef - readonly createPinia: UnwrapRef - readonly createProjection: UnwrapRef readonly createReactiveFn: UnwrapRef readonly createReusableTemplate: UnwrapRef readonly createSharedComposable: UnwrapRef @@ -411,19 +418,22 @@ declare module 'vue' { readonly debouncedWatch: UnwrapRef readonly defineAsyncComponent: UnwrapRef readonly defineComponent: UnwrapRef - readonly definePage: UnwrapRef - readonly defineStore: UnwrapRef + readonly draftIdempotencyKey: UnwrapRef + readonly draftSubmitterStorageKey: UnwrapRef readonly eagerComputed: UnwrapRef readonly effectScope: UnwrapRef readonly emailValidator: UnwrapRef + readonly evaluateConditionalLogic: UnwrapRef readonly extendRef: UnwrapRef + readonly extractErrorBody: UnwrapRef + readonly extractRetryAfterSeconds: UnwrapRef readonly formatDate: UnwrapRef readonly formatDateToMonthShort: UnwrapRef readonly generateDeviceFingerprint: UnwrapRef - readonly getActivePinia: UnwrapRef readonly getCurrentInstance: UnwrapRef readonly getCurrentScope: UnwrapRef readonly getDeviceName: UnwrapRef + readonly getValidatorsForField: UnwrapRef readonly h: UnwrapRef readonly hexToRgb: UnwrapRef readonly ignorableWatch: UnwrapRef @@ -433,30 +443,22 @@ declare module 'vue' { readonly isDefined: UnwrapRef readonly isEmpty: UnwrapRef readonly isEmptyArray: UnwrapRef + readonly isFieldValueEmpty: UnwrapRef readonly isNullOrUndefined: UnwrapRef readonly isObject: UnwrapRef readonly isProxy: UnwrapRef readonly isReactive: UnwrapRef readonly isReadonly: UnwrapRef readonly isRef: UnwrapRef + readonly isStepValid: UnwrapRef readonly isToday: UnwrapRef readonly kFormatter: UnwrapRef readonly lengthValidator: UnwrapRef - readonly logicAnd: UnwrapRef - readonly logicNot: UnwrapRef - readonly logicOr: UnwrapRef readonly makeDestructurable: UnwrapRef - readonly mapActions: UnwrapRef - readonly mapGetters: UnwrapRef - readonly mapState: UnwrapRef - readonly mapStores: UnwrapRef - readonly mapWritableState: UnwrapRef readonly markRaw: UnwrapRef readonly nextTick: UnwrapRef readonly onActivated: UnwrapRef readonly onBeforeMount: UnwrapRef - readonly onBeforeRouteLeave: UnwrapRef - readonly onBeforeRouteUpdate: UnwrapRef readonly onBeforeUnmount: UnwrapRef readonly onBeforeUpdate: UnwrapRef readonly onClickOutside: UnwrapRef @@ -500,12 +502,10 @@ declare module 'vue' { readonly resolveUnref: UnwrapRef readonly resolveVuetifyTheme: UnwrapRef readonly rgbaToHex: UnwrapRef - readonly setActivePinia: UnwrapRef - readonly setMapStoreSuffix: UnwrapRef + readonly runValidators: UnwrapRef readonly shallowReactive: UnwrapRef readonly shallowReadonly: UnwrapRef readonly shallowRef: UnwrapRef - readonly storeToRefs: UnwrapRef readonly syncRef: UnwrapRef readonly syncRefs: UnwrapRef readonly templateRef: UnwrapRef @@ -526,7 +526,6 @@ declare module 'vue' { readonly unrefElement: UnwrapRef readonly until: UnwrapRef readonly urlValidator: UnwrapRef - readonly useAbs: UnwrapRef readonly useActiveElement: UnwrapRef readonly useAnimate: UnwrapRef readonly useArrayDifference: UnwrapRef @@ -544,7 +543,6 @@ declare module 'vue' { readonly useAsyncQueue: UnwrapRef readonly useAsyncState: UnwrapRef readonly useAttrs: UnwrapRef - readonly useAverage: UnwrapRef readonly useBase64: UnwrapRef readonly useBattery: UnwrapRef readonly useBluetooth: UnwrapRef @@ -552,8 +550,6 @@ declare module 'vue' { readonly useBroadcastChannel: UnwrapRef readonly useBrowserLocation: UnwrapRef readonly useCached: UnwrapRef - readonly useCeil: UnwrapRef - readonly useClamp: UnwrapRef readonly useClipboard: UnwrapRef readonly useClipboardItems: UnwrapRef readonly useCloned: UnwrapRef @@ -592,15 +588,15 @@ declare module 'vue' { readonly useFetch: UnwrapRef readonly useFileDialog: UnwrapRef readonly useFileSystemAccess: UnwrapRef - readonly useFloor: UnwrapRef readonly useFocus: UnwrapRef readonly useFocusWithin: UnwrapRef + readonly useFormDraft: UnwrapRef + readonly useFormSteps: UnwrapRef readonly useFps: UnwrapRef readonly useFullscreen: UnwrapRef readonly useGamepad: UnwrapRef readonly useGenerateImageVariant: UnwrapRef readonly useGeolocation: UnwrapRef - readonly useI18n: UnwrapRef readonly useId: UnwrapRef readonly useIdle: UnwrapRef readonly useImage: UnwrapRef @@ -613,13 +609,10 @@ declare module 'vue' { readonly useLocalStorage: UnwrapRef readonly useMagicKeys: UnwrapRef readonly useManualRefHistory: UnwrapRef - readonly useMath: UnwrapRef - readonly useMax: UnwrapRef readonly useMediaControls: UnwrapRef readonly useMediaQuery: UnwrapRef readonly useMemoize: UnwrapRef readonly useMemory: UnwrapRef - readonly useMin: UnwrapRef readonly useModel: UnwrapRef readonly useMounted: UnwrapRef readonly useMouse: UnwrapRef @@ -640,21 +633,16 @@ declare module 'vue' { readonly usePointer: UnwrapRef readonly usePointerLock: UnwrapRef readonly usePointerSwipe: UnwrapRef - readonly usePrecision: UnwrapRef readonly usePreferredColorScheme: UnwrapRef readonly usePreferredContrast: UnwrapRef readonly usePreferredDark: UnwrapRef readonly usePreferredLanguages: UnwrapRef readonly usePreferredReducedMotion: UnwrapRef readonly usePrevious: UnwrapRef - readonly useProjection: UnwrapRef readonly useRafFn: UnwrapRef readonly useRefHistory: UnwrapRef readonly useResizeObserver: UnwrapRef readonly useResponsiveLeftSidebar: UnwrapRef - readonly useRound: UnwrapRef - readonly useRoute: UnwrapRef - readonly useRouter: UnwrapRef readonly useScreenOrientation: UnwrapRef readonly useScreenSafeArea: UnwrapRef readonly useScriptTag: UnwrapRef @@ -668,9 +656,9 @@ declare module 'vue' { readonly useSpeechRecognition: UnwrapRef readonly useSpeechSynthesis: UnwrapRef readonly useStepper: UnwrapRef + readonly useStorage: UnwrapRef readonly useStorageAsync: UnwrapRef readonly useStyleTag: UnwrapRef - readonly useSum: UnwrapRef readonly useSupported: UnwrapRef readonly useSwipe: UnwrapRef readonly useTemplateRef: UnwrapRef @@ -691,7 +679,6 @@ declare module 'vue' { readonly useToString: UnwrapRef readonly useToggle: UnwrapRef readonly useTransition: UnwrapRef - readonly useTrunc: UnwrapRef readonly useUrlSearchParams: UnwrapRef readonly useUserMedia: UnwrapRef readonly useVModel: UnwrapRef diff --git a/apps/portal/components.d.ts b/apps/portal/components.d.ts index 5f07ae12..dc5b97e8 100644 --- a/apps/portal/components.d.ts +++ b/apps/portal/components.d.ts @@ -34,6 +34,24 @@ declare module 'vue' { DialogCloseBtn: typeof import('./src/@core/components/DialogCloseBtn.vue')['default'] DropZone: typeof import('./src/@core/components/DropZone.vue')['default'] EventCard: typeof import('./src/components/portal/EventCard.vue')['default'] + FieldBoolean: typeof import('./src/components/public-form/FieldBoolean.vue')['default'] + FieldCheckboxList: typeof import('./src/components/public-form/FieldCheckboxList.vue')['default'] + FieldDate: typeof import('./src/components/public-form/FieldDate.vue')['default'] + FieldEmail: typeof import('./src/components/public-form/FieldEmail.vue')['default'] + FieldHeading: typeof import('./src/components/public-form/FieldHeading.vue')['default'] + FieldMultiselect: typeof import('./src/components/public-form/FieldMultiselect.vue')['default'] + FieldNumber: typeof import('./src/components/public-form/FieldNumber.vue')['default'] + FieldParagraph: typeof import('./src/components/public-form/FieldParagraph.vue')['default'] + FieldPhone: typeof import('./src/components/public-form/FieldPhone.vue')['default'] + FieldRadio: typeof import('./src/components/public-form/FieldRadio.vue')['default'] + FieldRenderer: typeof import('./src/components/public-form/FieldRenderer.vue')['default'] + FieldSelect: typeof import('./src/components/public-form/FieldSelect.vue')['default'] + FieldText: typeof import('./src/components/public-form/FieldText.vue')['default'] + FieldTextarea: typeof import('./src/components/public-form/FieldTextarea.vue')['default'] + FieldUrl: typeof import('./src/components/public-form/FieldUrl.vue')['default'] + FormConfirmation: typeof import('./src/components/public-form/FormConfirmation.vue')['default'] + FormErrorState: typeof import('./src/components/public-form/FormErrorState.vue')['default'] + FormStepper: typeof import('./src/components/public-form/FormStepper.vue')['default'] I18n: typeof import('./src/@core/components/I18n.vue')['default'] InformatieTab: typeof import('./src/components/event/InformatieTab.vue')['default'] MfaChallengeCard: typeof import('./src/components/auth/MfaChallengeCard.vue')['default'] @@ -51,6 +69,7 @@ declare module 'vue' { ScrollToTop: typeof import('./src/@core/components/ScrollToTop.vue')['default'] Shortcuts: typeof import('./src/@core/components/Shortcuts.vue')['default'] StatusCard: typeof import('./src/components/portal/StatusCard.vue')['default'] + SubmitterDetails: typeof import('./src/components/public-form/SubmitterDetails.vue')['default'] TablePagination: typeof import('./src/@core/components/TablePagination.vue')['default'] TheCustomizer: typeof import('./src/@core/components/TheCustomizer.vue')['default'] ThemeSwitcher: typeof import('./src/@core/components/ThemeSwitcher.vue')['default'] diff --git a/apps/portal/package.json b/apps/portal/package.json index d44bb7e1..96cb8180 100644 --- a/apps/portal/package.json +++ b/apps/portal/package.json @@ -8,6 +8,8 @@ "build": "vite build", "preview": "vite preview --port 5050", "typecheck": "vue-tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", "lint": "eslint . -c .eslintrc.cjs --fix --ext .ts,.js,.cjs,.vue,.tsx,.jsx", "build:icons": "tsx src/plugins/iconify/build-icons.ts", "msw:init": "msw init public/ --save", @@ -36,6 +38,7 @@ "cookie-es": "1.2.2", "destr": "2.0.5", "eslint-plugin-regexp": "2.10.0", + "flatpickr": "^4.6.13", "jwt-decode": "4.0.0", "mapbox-gl": "3.5.2", "ofetch": "1.5.0", @@ -91,6 +94,7 @@ "@typescript-eslint/parser": "7.18.0", "@vitejs/plugin-vue": "6.0.1", "@vitejs/plugin-vue-jsx": "5.1.1", + "@vue/test-utils": "^2.4.6", "eslint": "8.57.1", "eslint-config-airbnb-base": "15.0.0", "eslint-import-resolver-typescript": "3.10.1", @@ -101,6 +105,7 @@ "eslint-plugin-sonarjs": "0.24.0", "eslint-plugin-unicorn": "51.0.1", "eslint-plugin-vue": "9.33.0", + "jsdom": "^29.0.2", "msw": "2.6.8", "postcss-html": "1.8.0", "postcss-scss": "4.0.9", @@ -121,6 +126,7 @@ "vite-plugin-vue-meta-layouts": "0.6.1", "vite-plugin-vuetify": "2.1.2", "vite-svg-loader": "5.1.0", + "vitest": "^3.2.4", "vue-shepherd": "3.0.0", "vue-tsc": "3.1.2" }, diff --git a/apps/portal/pnpm-lock.yaml b/apps/portal/pnpm-lock.yaml index d250b930..6d55bf31 100644 --- a/apps/portal/pnpm-lock.yaml +++ b/apps/portal/pnpm-lock.yaml @@ -78,6 +78,9 @@ importers: eslint-plugin-regexp: specifier: 2.10.0 version: 2.10.0(eslint@8.57.1) + flatpickr: + specifier: ^4.6.13 + version: 4.6.13 jwt-decode: specifier: 4.0.0 version: 4.0.0 @@ -238,6 +241,9 @@ importers: '@vitejs/plugin-vue-jsx': specifier: 5.1.1 version: 5.1.1(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.3)) + '@vue/test-utils': + specifier: ^2.4.6 + version: 2.4.6 eslint: specifier: 8.57.1 version: 8.57.1 @@ -268,6 +274,9 @@ importers: eslint-plugin-vue: specifier: 9.33.0 version: 9.33.0(eslint@8.57.1) + jsdom: + specifier: ^29.0.2 + version: 29.0.2 msw: specifier: 2.6.8 version: 2.6.8(@types/node@24.9.2)(typescript@5.9.3) @@ -328,6 +337,9 @@ importers: vite-svg-loader: specifier: 5.1.0 version: 5.1.0(vue@3.5.22(typescript@5.9.3)) + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@24.9.2)(jsdom@29.0.2)(msw@2.6.8(@types/node@24.9.2)(typescript@5.9.3))(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1) vue-shepherd: specifier: 3.0.0 version: 3.0.0(vue@3.5.22(typescript@5.9.3)) @@ -365,6 +377,21 @@ packages: '@antfu/utils@8.1.1': resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.0.10': + resolution: {integrity: sha512-KyOb19eytNSELkmdqzZZUXWCU25byIlOld5qVFg0RYdS0T3tt7jeDByxk9hIAC73frclD8GKrHttr0SUjKCCdQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + '@babel/code-frame@7.27.1': resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} @@ -505,6 +532,10 @@ packages: resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + '@bundled-es-modules/cookie@2.0.1': resolution: {integrity: sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==} @@ -523,16 +554,52 @@ packages: '@casl/ability': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.1.0 || ^6.0.0 vue: ^3.0.0 + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + '@csstools/css-parser-algorithms@2.7.1': resolution: {integrity: sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==} engines: {node: ^14 || ^16 || >=18} peerDependencies: '@csstools/css-tokenizer': ^2.4.1 + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + '@csstools/css-tokenizer@2.4.1': resolution: {integrity: sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==} engines: {node: ^14 || ^16 || >=18} + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + '@csstools/media-query-list-parser@2.1.13': resolution: {integrity: sha512-XaHr+16KRU9Gf8XLi3q8kDlI18d5vzKSKCY510Vrtc9iNR0NJzbY9hhTmwhzYZj/ZwGL4VmB3TA9hJW0Um2qFA==} engines: {node: ^14 || ^16 || >=18} @@ -736,6 +803,15 @@ packages: resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -903,6 +979,10 @@ packages: vue-i18n: optional: true + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -968,6 +1048,9 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@one-ini/wasm@0.1.1': + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + '@open-draft/deferred-promise@2.2.0': resolution: {integrity: sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==} @@ -977,6 +1060,10 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@pkgr/core@0.1.2': resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -1390,9 +1477,15 @@ packages: '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1781,6 +1874,35 @@ packages: vite: ^5.0.0 || ^6.0.0 || ^7.0.0 vue: ^3.2.25 + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@volar/language-core@2.4.23': resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} @@ -1875,6 +1997,9 @@ packages: '@vue/shared@3.5.22': resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} + '@vue/test-utils@2.4.6': + resolution: {integrity: sha512-FMxEjOpYNYiFe0GkaHsnJPXFHxQ6m4t8vI/ElPGpMWxZKpmRvQ33OIrvRXemy6yha03RxhOlQuy+gZMC3CQSow==} + '@vuetify/loader-shared@2.1.1': resolution: {integrity: sha512-jSZTzTYaoiv8iwonFCVZQ0YYX/M+Uyl4ng+C4egMJT0Hcmh9gIxJL89qfZICDeo3g0IhqrvipW2FFKKRDMtVcA==} peerDependencies: @@ -1896,6 +2021,10 @@ packages: '@yr/monotone-cubic-spline@1.0.3': resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==} + abbrev@2.0.0: + resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1927,6 +2056,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + ansis@4.2.0: resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} engines: {node: '>=14'} @@ -1973,6 +2106,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + ast-kit@1.4.3: resolution: {integrity: sha512-MdJqjpodkS5J149zN0Po+HPshkTdUyrvF7CKTafUgv69vBSPtncrj+3IiUgqdd7ElIEkbeXCsEouBUwLrw9Ilg==} engines: {node: '>=16.14.0'} @@ -2009,6 +2146,9 @@ packages: resolution: {integrity: sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==} hasBin: true + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2051,6 +2191,10 @@ packages: resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} engines: {node: '>=18'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2081,6 +2225,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2107,6 +2255,10 @@ packages: cheap-ruler@4.0.0: resolution: {integrity: sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -2162,6 +2314,10 @@ packages: comma-separated-tokens@2.0.3: resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@10.0.1: + resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} + engines: {node: '>=14'} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -2179,6 +2335,9 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + confusing-browser-globals@1.0.11: resolution: {integrity: sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA==} @@ -2234,6 +2393,10 @@ packages: resolution: {integrity: sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + css-what@6.2.2: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} @@ -2253,6 +2416,10 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -2286,6 +2453,13 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2370,6 +2544,14 @@ packages: earcut@3.0.2: resolution: {integrity: sha512-X7hshQbLyMJ/3RPhyObLARM2sNxxmRALLKx1+NVFFnQ9gKzmCrxm9+uLIAdBcvc8FNLpctqlQ2V6AE92Ol9UDQ==} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + editorconfig@1.0.7: + resolution: {integrity: sha512-e0GOtq/aTQhVdNyDU9e02+wz9oDDM+SIOQxWME2QRjzRX5yyLAuHDE+0aE8vHb9XRC8XD37eO2u57+F09JqFhw==} + engines: {node: '>=14'} + hasBin: true + electron-to-chromium@1.5.244: resolution: {integrity: sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==} @@ -2379,6 +2561,9 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + encoding-sniffer@0.2.1: resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} @@ -2415,6 +2600,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2734,6 +2922,10 @@ packages: resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + exsolve@1.0.7: resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} @@ -2831,6 +3023,10 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + form-data@4.0.5: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} @@ -2909,6 +3105,11 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -2996,6 +3197,10 @@ packages: hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-tags@3.3.1: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} @@ -3172,6 +3377,9 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3230,6 +3438,18 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + js-beautify@1.15.4: + resolution: {integrity: sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==} + engines: {node: '>=14'} + hasBin: true + + js-cookie@3.0.5: + resolution: {integrity: sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==} + engines: {node: '>=14'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -3248,6 +3468,15 @@ packages: resolution: {integrity: sha512-iZ8Bdb84lWRuGHamRXFyML07r21pcwBrLkHEuHgEY5UbCouBwv7ECknDRKzsQIXMiqpPymqtIf8TC/shYKB5rw==} engines: {node: '>=12.0.0'} + jsdom@29.0.2: + resolution: {integrity: sha512-9VnGEBosc/ZpwyOsJBCQ/3I5p7Q5ngOY14a9bf5btenAORmZfDse1ZEheMiWcJ3h81+Fv7HmJFdS0szo/waF2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + jsesc@0.5.0: resolution: {integrity: sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==} hasBin: true @@ -3353,6 +3582,16 @@ packages: lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} + engines: {node: 20 || >=22} + lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} @@ -3398,6 +3637,9 @@ packages: mdn-data@2.25.0: resolution: {integrity: sha512-T2LPsjgUE/tgMmRXREVmwsux89DwWfNjiynOeXuLd2mX6jphGQ2YE3Ukz7LQ2VOFKiVZU/Ee1GqzHiipZCjymw==} + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} @@ -3472,6 +3714,10 @@ packages: resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==} engines: {node: '>=8'} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} @@ -3542,6 +3788,11 @@ packages: node-releases@2.0.27: resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} + nopt@7.2.1: + resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + hasBin: true + normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} @@ -3634,6 +3885,9 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + package-manager-detector@1.5.0: resolution: {integrity: sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==} @@ -3661,6 +3915,9 @@ packages: parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parse5@8.0.0: + resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + path-browserify@1.0.1: resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} @@ -3683,6 +3940,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -3696,6 +3957,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + pbf@3.3.0: resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} hasBin: true @@ -3877,6 +4142,9 @@ packages: prosemirror-view@1.41.3: resolution: {integrity: sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==} + proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + protocol-buffers-schema@3.6.0: resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} @@ -4059,6 +4327,10 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scslre@0.3.0: resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} engines: {node: ^14.0.0 || >=16.0.0} @@ -4138,6 +4410,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -4187,10 +4462,16 @@ packages: stable-hash@0.0.5: resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -4202,6 +4483,10 @@ packages: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + string.prototype.trim@1.2.10: resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} engines: {node: '>= 0.4'} @@ -4244,6 +4529,9 @@ packages: strip-literal@2.1.1: resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + style-search@0.1.0: resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} @@ -4369,6 +4657,9 @@ packages: resolution: {integrity: sha512-RMeVUUjTQH+6N3ckimK93oxz6Sn5la4aDlgPzB+rBrG/smPdCTicXyhxa+woIpopz+jewEloiEE3lKo1h9w2YQ==} engines: {node: '>= 4.7.0'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.11.11: resolution: {integrity: sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4396,6 +4687,12 @@ packages: resolution: {integrity: sha512-hkcz3FjNJfKXjV4mjQ1OrXSLAehg8Hw+cEZclOVT+5c/cWQWImQ9wolzTjth+dmmDe++p3bme3fTxz6Q4Etsqw==} engines: {node: '>=12'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.1: resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} @@ -4403,12 +4700,31 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + tinyqueue@3.0.0: resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + tippy.js@6.3.7: resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + tldts-core@7.0.28: + resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==} + + tldts@7.0.28: + resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4421,6 +4737,14 @@ packages: resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} engines: {node: '>=6'} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -4521,6 +4845,10 @@ packages: resolution: {integrity: sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==} engines: {node: '>=18.17'} + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + unicorn-magic@0.3.0: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} @@ -4645,6 +4973,11 @@ packages: peerDependencies: vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite-plugin-inspect@11.3.3: resolution: {integrity: sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==} engines: {node: '>=14'} @@ -4725,6 +5058,34 @@ packages: yaml: optional: true + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vscode-uri@3.1.0: resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} @@ -4737,6 +5098,9 @@ packages: chart.js: ^4.1.1 vue: ^3.0.0-0 || ^2.7.0 + vue-component-type-helpers@2.2.12: + resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==} + vue-demi@0.14.10: resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} engines: {node: '>=12'} @@ -4822,9 +5186,17 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + webfontloader@1.6.28: resolution: {integrity: sha512-Egb0oFEga6f+nSgasH3E0M405Pzn6y3/9tOVanv/DLfa1YBIgcv90L18YyWnvXkRbIM17v5Kv6IT2N6g1x5tvQ==} + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -4837,6 +5209,14 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -4865,6 +5245,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -4877,6 +5262,10 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} @@ -4892,6 +5281,13 @@ packages: resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==} engines: {node: '>=12'} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@4.0.3: resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} @@ -5051,6 +5447,26 @@ snapshots: '@antfu/utils@8.1.1': {} + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.0.10': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + '@babel/code-frame@7.27.1': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -5242,6 +5658,10 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + '@bundled-es-modules/cookie@2.0.1': dependencies: cookie: 0.7.2 @@ -5264,12 +5684,36 @@ snapshots: '@casl/ability': 6.7.3 vue: 3.5.22(typescript@5.9.3) + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + '@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1)': dependencies: '@csstools/css-tokenizer': 2.4.1 + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + '@csstools/css-tokenizer@2.4.1': {} + '@csstools/css-tokenizer@4.0.0': {} + '@csstools/media-query-list-parser@2.1.13(@csstools/css-parser-algorithms@2.7.1(@csstools/css-tokenizer@2.4.1))(@csstools/css-tokenizer@2.4.1)': dependencies: '@csstools/css-parser-algorithms': 2.7.1(@csstools/css-tokenizer@2.4.1) @@ -5404,6 +5848,8 @@ snapshots: '@eslint/js@8.57.1': {} + '@exodus/bytes@1.15.0': {} + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -5591,6 +6037,15 @@ snapshots: vue: 3.5.22(typescript@5.9.3) vue-i18n: 11.1.12(vue@3.5.22(typescript@5.9.3)) + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5658,6 +6113,8 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@one-ini/wasm@0.1.1': {} + '@open-draft/deferred-promise@2.2.0': {} '@open-draft/logger@0.3.0': @@ -5667,6 +6124,9 @@ snapshots: '@open-draft/until@2.1.0': {} + '@pkgjs/parseargs@0.11.0': + optional: true + '@pkgr/core@0.1.2': {} '@pkgr/core@0.2.9': {} @@ -6048,8 +6508,15 @@ snapshots: tslib: 2.8.1 optional: true + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + '@types/cookie@0.6.0': {} + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/geojson@7946.0.16': {} @@ -6479,6 +6946,49 @@ snapshots: vite: 7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1) vue: 3.5.22(typescript@5.9.3) + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(msw@2.6.8(@types/node@24.9.2)(typescript@5.9.3))(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.6.8(@types/node@24.9.2)(typescript@5.9.3) + vite: 7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + '@volar/language-core@2.4.23': dependencies: '@volar/source-map': 2.4.23 @@ -6643,6 +7153,11 @@ snapshots: '@vue/shared@3.5.22': {} + '@vue/test-utils@2.4.6': + dependencies: + js-beautify: 1.15.4 + vue-component-type-helpers: 2.2.12 + '@vuetify/loader-shared@2.1.1(vue@3.5.22(typescript@5.9.3))(vuetify@3.10.8)': dependencies: upath: 2.0.1 @@ -6678,6 +7193,8 @@ snapshots: '@yr/monotone-cubic-spline@1.0.3': {} + abbrev@2.0.0: {} + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -6708,6 +7225,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.3: {} + ansis@4.2.0: {} anymatch@3.1.3: @@ -6781,6 +7300,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assertion-error@2.0.1: {} + ast-kit@1.4.3: dependencies: '@babel/parser': 7.28.5 @@ -6815,6 +7336,10 @@ snapshots: baseline-browser-mapping@2.8.21: {} + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + binary-extensions@2.3.0: {} birpc@2.6.1: {} @@ -6859,6 +7384,8 @@ snapshots: dependencies: run-applescript: 7.1.0 + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -6886,6 +7413,14 @@ snapshots: ccount@2.0.1: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6907,6 +7442,8 @@ snapshots: cheap-ruler@4.0.0: {} + check-error@2.1.3: {} + cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 @@ -6980,6 +7517,8 @@ snapshots: comma-separated-tokens@2.0.3: {} + commander@10.0.1: {} + commander@7.2.0: {} comment-parser@1.4.1: {} @@ -6990,6 +7529,11 @@ snapshots: confbox@0.2.2: {} + config-chain@1.1.13: + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + confusing-browser-globals@1.0.11: {} convert-source-map@2.0.0: {} @@ -7048,6 +7592,11 @@ snapshots: mdn-data: 2.12.2 source-map-js: 1.2.1 + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + css-what@6.2.2: {} csscolorparser@1.0.3: {} @@ -7060,6 +7609,13 @@ snapshots: csstype@3.1.3: {} + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -7088,6 +7644,10 @@ snapshots: decamelize@1.2.0: {} + decimal.js@10.6.0: {} + + deep-eql@5.0.2: {} + deep-is@0.1.4: {} deepmerge-ts@5.1.0: {} @@ -7167,12 +7727,23 @@ snapshots: earcut@3.0.2: {} + eastasianwidth@0.2.0: {} + + editorconfig@1.0.7: + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.5 + semver: 7.7.4 + electron-to-chromium@1.5.244: {} emoji-regex-xs@1.0.0: {} emoji-regex@8.0.0: {} + emoji-regex@9.2.2: {} + encoding-sniffer@0.2.1: dependencies: iconv-lite: 0.6.3 @@ -7255,6 +7826,8 @@ snapshots: es-errors@1.3.0: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -7786,6 +8359,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + expect-type@1.3.0: {} + exsolve@1.0.7: {} extract-zip@2.0.1: @@ -7877,6 +8452,11 @@ snapshots: dependencies: is-callable: 1.2.7 + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + form-data@4.0.5: dependencies: asynckit: 0.4.0 @@ -7966,6 +8546,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -8061,6 +8650,12 @@ snapshots: hosted-git-info@2.8.9: {} + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.0 + transitivePeerDependencies: + - '@noble/hashes' + html-tags@3.3.1: {} html-void-elements@3.0.0: {} @@ -8225,6 +8820,8 @@ snapshots: is-plain-object@5.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -8278,6 +8875,22 @@ snapshots: isexe@2.0.0: {} + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + js-beautify@1.15.4: + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.7 + glob: 10.5.0 + js-cookie: 3.0.5 + nopt: 7.2.1 + + js-cookie@3.0.5: {} + js-tokens@4.0.0: {} js-tokens@9.0.1: {} @@ -8290,6 +8903,32 @@ snapshots: jsdoc-type-pratt-parser@4.8.0: {} + jsdom@29.0.2: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.0.10 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + '@exodus/bytes': 1.15.0 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.3.5 + parse5: 8.0.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + jsesc@0.5.0: {} jsesc@3.1.0: {} @@ -8375,6 +9014,12 @@ snapshots: lodash@4.17.21: {} + loupe@3.2.1: {} + + lru-cache@10.4.3: {} + + lru-cache@11.3.5: {} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 @@ -8464,6 +9109,8 @@ snapshots: mdn-data@2.25.0: {} + mdn-data@2.27.1: {} + mdurl@2.0.0: {} meow@13.2.0: {} @@ -8533,6 +9180,8 @@ snapshots: minipass@5.0.0: {} + minipass@7.1.3: {} + minizlib@2.1.2: dependencies: minipass: 3.3.6 @@ -8603,6 +9252,10 @@ snapshots: node-releases@2.0.27: {} + nopt@7.2.1: + dependencies: + abbrev: 2.0.0 + normalize-package-data@2.5.0: dependencies: hosted-git-info: 2.8.9 @@ -8723,6 +9376,8 @@ snapshots: p-try@2.2.0: {} + package-json-from-dist@1.0.1: {} + package-manager-detector@1.5.0: {} parent-module@1.0.1: @@ -8760,6 +9415,10 @@ snapshots: dependencies: entities: 6.0.1 + parse5@8.0.0: + dependencies: + entities: 6.0.1 + path-browserify@1.0.1: {} path-exists@4.0.0: {} @@ -8772,6 +9431,11 @@ snapshots: path-parse@1.0.7: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + path-to-regexp@6.3.0: {} path-type@4.0.0: {} @@ -8780,6 +9444,8 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.1: {} + pbf@3.3.0: dependencies: ieee754: 1.2.1 @@ -8986,6 +9652,8 @@ snapshots: prosemirror-state: 1.4.4 prosemirror-transform: 1.10.4 + proto-list@1.2.4: {} + protocol-buffers-schema@3.6.0: {} proxy-from-env@2.1.0: {} @@ -9197,6 +9865,10 @@ snapshots: immutable: 4.3.7 source-map-js: 1.2.1 + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scslre@0.3.0: dependencies: '@eslint-community/regexpp': 4.12.2 @@ -9295,6 +9967,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@4.1.0: {} sirv@3.0.2: @@ -9341,8 +10015,12 @@ snapshots: stable-hash@0.0.5: {} + stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -9356,6 +10034,12 @@ snapshots: is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + string.prototype.trim@1.2.10: dependencies: call-bind: 1.0.8 @@ -9406,6 +10090,10 @@ snapshots: dependencies: js-tokens: 9.0.1 + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + style-search@0.1.0: {} stylelint-config-idiomatic-order@10.0.0(stylelint@16.8.0(typescript@5.9.3)): @@ -9570,6 +10258,8 @@ snapshots: swiper@11.2.10: {} + symbol-tree@3.2.4: {} + synckit@0.11.11: dependencies: '@pkgr/core': 0.2.9 @@ -9602,6 +10292,10 @@ snapshots: tiny-lru@11.4.5: {} + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + tinyexec@1.0.1: {} tinyglobby@0.2.15: @@ -9609,12 +10303,24 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinypool@1.1.1: {} + tinyqueue@3.0.0: {} + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + tippy.js@6.3.7: dependencies: '@popperjs/core': 2.11.8 + tldts-core@7.0.28: {} + + tldts@7.0.28: + dependencies: + tldts-core: 7.0.28 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -9628,6 +10334,14 @@ snapshots: universalify: 0.2.0 url-parse: 1.5.10 + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.28 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + trim-lines@3.0.1: {} ts-api-utils@1.4.3(typescript@5.9.3): @@ -9729,6 +10443,8 @@ snapshots: undici@6.22.0: {} + undici@7.25.0: {} + unicorn-magic@0.3.0: {} unimport@3.14.6(rollup@4.52.5): @@ -9933,6 +10649,27 @@ snapshots: dependencies: vite: 7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1) + vite-node@3.2.4(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-plugin-inspect@11.3.3(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1)): dependencies: ansis: 4.2.0 @@ -10015,6 +10752,48 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 + vitest@3.2.4(@types/node@24.9.2)(jsdom@29.0.2)(msw@2.6.8(@types/node@24.9.2)(typescript@5.9.3))(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(msw@2.6.8(@types/node@24.9.2)(typescript@5.9.3))(vite@7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.1.12(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.9.2)(sass@1.76.0)(tsx@4.20.6)(yaml@2.8.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.9.2 + jsdom: 29.0.2 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vscode-uri@3.1.0: {} vt-pbf@3.1.3: @@ -10028,6 +10807,8 @@ snapshots: chart.js: 4.5.1 vue: 3.5.22(typescript@5.9.3) + vue-component-type-helpers@2.2.12: {} + vue-demi@0.14.10(vue@3.5.22(typescript@5.9.3)): dependencies: vue: 3.5.22(typescript@5.9.3) @@ -10104,8 +10885,14 @@ snapshots: w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + webfontloader@1.6.28: {} + webidl-conversions@8.0.1: {} + webpack-virtual-modules@0.6.2: {} whatwg-encoding@3.1.1: @@ -10114,6 +10901,16 @@ snapshots: whatwg-mimetype@4.0.0: {} + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -10165,6 +10962,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@6.2.0: @@ -10179,6 +10981,12 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + wrappy@1.0.2: {} write-file-atomic@5.0.1: @@ -10192,6 +11000,10 @@ snapshots: xml-name-validator@4.0.0: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + y18n@4.0.3: {} y18n@5.0.8: {} diff --git a/apps/portal/src/components/public-form/FieldBoolean.vue b/apps/portal/src/components/public-form/FieldBoolean.vue new file mode 100644 index 00000000..6f825894 --- /dev/null +++ b/apps/portal/src/components/public-form/FieldBoolean.vue @@ -0,0 +1,40 @@ + + + diff --git a/apps/portal/src/components/public-form/FieldCheckboxList.vue b/apps/portal/src/components/public-form/FieldCheckboxList.vue new file mode 100644 index 00000000..a5a733fa --- /dev/null +++ b/apps/portal/src/components/public-form/FieldCheckboxList.vue @@ -0,0 +1,107 @@ + + + diff --git a/apps/portal/src/components/public-form/FieldDate.vue b/apps/portal/src/components/public-form/FieldDate.vue new file mode 100644 index 00000000..318cda4c --- /dev/null +++ b/apps/portal/src/components/public-form/FieldDate.vue @@ -0,0 +1,37 @@ + + + diff --git a/apps/portal/src/components/public-form/FieldEmail.vue b/apps/portal/src/components/public-form/FieldEmail.vue new file mode 100644 index 00000000..6fe03648 --- /dev/null +++ b/apps/portal/src/components/public-form/FieldEmail.vue @@ -0,0 +1,38 @@ + + + diff --git a/apps/portal/src/components/public-form/FieldHeading.vue b/apps/portal/src/components/public-form/FieldHeading.vue new file mode 100644 index 00000000..cb5b9fcc --- /dev/null +++ b/apps/portal/src/components/public-form/FieldHeading.vue @@ -0,0 +1,22 @@ + + + diff --git a/apps/portal/src/components/public-form/FieldMultiselect.vue b/apps/portal/src/components/public-form/FieldMultiselect.vue new file mode 100644 index 00000000..48d579fd --- /dev/null +++ b/apps/portal/src/components/public-form/FieldMultiselect.vue @@ -0,0 +1,67 @@ + + + diff --git a/apps/portal/src/components/public-form/FieldNumber.vue b/apps/portal/src/components/public-form/FieldNumber.vue new file mode 100644 index 00000000..933315f4 --- /dev/null +++ b/apps/portal/src/components/public-form/FieldNumber.vue @@ -0,0 +1,54 @@ + + + diff --git a/apps/portal/src/components/public-form/FieldParagraph.vue b/apps/portal/src/components/public-form/FieldParagraph.vue new file mode 100644 index 00000000..646e1a3e --- /dev/null +++ b/apps/portal/src/components/public-form/FieldParagraph.vue @@ -0,0 +1,13 @@ + + + diff --git a/apps/portal/src/components/public-form/FieldPhone.vue b/apps/portal/src/components/public-form/FieldPhone.vue new file mode 100644 index 00000000..e33807d4 --- /dev/null +++ b/apps/portal/src/components/public-form/FieldPhone.vue @@ -0,0 +1,39 @@ + + + diff --git a/apps/portal/src/components/public-form/FieldRadio.vue b/apps/portal/src/components/public-form/FieldRadio.vue new file mode 100644 index 00000000..6cb9b362 --- /dev/null +++ b/apps/portal/src/components/public-form/FieldRadio.vue @@ -0,0 +1,85 @@ + + + diff --git a/apps/portal/src/components/public-form/FieldRenderer.vue b/apps/portal/src/components/public-form/FieldRenderer.vue new file mode 100644 index 00000000..9f493a80 --- /dev/null +++ b/apps/portal/src/components/public-form/FieldRenderer.vue @@ -0,0 +1,208 @@ + + + diff --git a/apps/portal/src/components/public-form/FieldSelect.vue b/apps/portal/src/components/public-form/FieldSelect.vue new file mode 100644 index 00000000..2bbbb603 --- /dev/null +++ b/apps/portal/src/components/public-form/FieldSelect.vue @@ -0,0 +1,65 @@ + + + diff --git a/apps/portal/src/components/public-form/FieldText.vue b/apps/portal/src/components/public-form/FieldText.vue new file mode 100644 index 00000000..dc5f92d5 --- /dev/null +++ b/apps/portal/src/components/public-form/FieldText.vue @@ -0,0 +1,35 @@ + + + diff --git a/apps/portal/src/components/public-form/FieldTextarea.vue b/apps/portal/src/components/public-form/FieldTextarea.vue new file mode 100644 index 00000000..9553c493 --- /dev/null +++ b/apps/portal/src/components/public-form/FieldTextarea.vue @@ -0,0 +1,37 @@ + + + diff --git a/apps/portal/src/components/public-form/FieldUrl.vue b/apps/portal/src/components/public-form/FieldUrl.vue new file mode 100644 index 00000000..3fda8e50 --- /dev/null +++ b/apps/portal/src/components/public-form/FieldUrl.vue @@ -0,0 +1,38 @@ + + + diff --git a/apps/portal/src/components/public-form/FormConfirmation.vue b/apps/portal/src/components/public-form/FormConfirmation.vue new file mode 100644 index 00000000..0b7badba --- /dev/null +++ b/apps/portal/src/components/public-form/FormConfirmation.vue @@ -0,0 +1,114 @@ + + + diff --git a/apps/portal/src/components/public-form/FormErrorState.vue b/apps/portal/src/components/public-form/FormErrorState.vue new file mode 100644 index 00000000..df42f815 --- /dev/null +++ b/apps/portal/src/components/public-form/FormErrorState.vue @@ -0,0 +1,118 @@ + + + diff --git a/apps/portal/src/components/public-form/FormStepper.vue b/apps/portal/src/components/public-form/FormStepper.vue new file mode 100644 index 00000000..436419a2 --- /dev/null +++ b/apps/portal/src/components/public-form/FormStepper.vue @@ -0,0 +1,40 @@ + + + diff --git a/apps/portal/src/components/public-form/SubmitterDetails.vue b/apps/portal/src/components/public-form/SubmitterDetails.vue new file mode 100644 index 00000000..85640ca0 --- /dev/null +++ b/apps/portal/src/components/public-form/SubmitterDetails.vue @@ -0,0 +1,80 @@ + + + diff --git a/apps/portal/src/composables/api/usePublicForm.ts b/apps/portal/src/composables/api/usePublicForm.ts new file mode 100644 index 00000000..79b6c18f --- /dev/null +++ b/apps/portal/src/composables/api/usePublicForm.ts @@ -0,0 +1,110 @@ +import { useMutation, useQuery } from '@tanstack/vue-query' +import type { AxiosError } from 'axios' +import type { MaybeRefOrGetter } from 'vue' +import { apiClient } from '@/lib/axios' +import type { + PublicFormErrorBody, + PublicFormSchema, + PublicFormSubmission, + SaveDraftBody, + StartDraftBody, + SubmitBody, +} from '@/types/formBuilder' + +interface ApiResponse { + data: T + message?: string + success?: boolean +} + +/** + * The backend standardises public-form errors as + * { message, code?, errors? } + * See api/app/Exceptions/FormBuilder/PublicFormApiException.php. + */ +export type PublicFormAxiosError = AxiosError + +const TERMINAL_STATUSES = new Set([404, 410, 409]) + +export function useFetchPublicFormSchema(token: MaybeRefOrGetter) { + return useQuery({ + queryKey: ['public-form-schema', () => toValue(token)], + queryFn: async (): Promise => { + const t = toValue(token) + if (!t) throw new Error('Missing public_token') + const { data } = await apiClient.get>(`/public/forms/${t}`) + + return data.data + }, + enabled: () => !!toValue(token), + retry: (failureCount, error) => { + const status = (error as PublicFormAxiosError | undefined)?.response?.status + if (status && TERMINAL_STATUSES.has(status)) return false + + return failureCount < 1 + }, + staleTime: 1000 * 60 * 5, + }) +} + +export function useCreateFormDraft(token: MaybeRefOrGetter) { + return useMutation({ + mutationFn: async (body: StartDraftBody): Promise => { + const t = toValue(token) + if (!t) throw new Error('Missing public_token') + const { data } = await apiClient.post>( + `/public/forms/${t}/submissions`, + body, + ) + + return data.data + }, + }) +} + +export function useSaveFormDraft(token: MaybeRefOrGetter) { + return useMutation({ + mutationFn: async ({ submissionId, body }: { submissionId: string; body: SaveDraftBody }): Promise => { + const t = toValue(token) + if (!t) throw new Error('Missing public_token') + const { data } = await apiClient.put>( + `/public/forms/${t}/submissions/${submissionId}`, + body, + ) + + return data.data + }, + }) +} + +export function useSubmitForm(token: MaybeRefOrGetter) { + return useMutation({ + mutationFn: async ({ submissionId, body }: { submissionId: string; body: SubmitBody }): Promise => { + const t = toValue(token) + if (!t) throw new Error('Missing public_token') + const { data } = await apiClient.post>( + `/public/forms/${t}/submissions/${submissionId}/submit`, + body, + ) + + return data.data + }, + }) +} + +export function extractErrorBody(err: unknown): PublicFormErrorBody | null { + const axiosErr = err as PublicFormAxiosError | undefined + const body = axiosErr?.response?.data + if (body && typeof body === 'object' && 'message' in body) return body as PublicFormErrorBody + + return null +} + +export function extractRetryAfterSeconds(err: unknown): number | null { + const axiosErr = err as PublicFormAxiosError | undefined + const raw = axiosErr?.response?.headers?.['retry-after'] + if (!raw) return null + const n = Number(raw) + + return Number.isFinite(n) && n >= 0 ? n : null +} diff --git a/apps/portal/src/composables/useConditionalLogic.ts b/apps/portal/src/composables/useConditionalLogic.ts new file mode 100644 index 00000000..1b592d07 --- /dev/null +++ b/apps/portal/src/composables/useConditionalLogic.ts @@ -0,0 +1,127 @@ +import type { + ConditionalGroup, + ConditionalLogic, + ConditionalOperator, + ConditionalRule, + FormValues, +} from '@/types/formBuilder' + +function isEmptyValue(v: unknown): boolean { + if (v === null || v === undefined) return true + if (typeof v === 'string') return v.length === 0 + if (Array.isArray(v)) return v.length === 0 + + return false +} + +function toComparable(v: unknown): string | number | boolean { + if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return v + + return String(v ?? '') +} + +function evaluateRule(rule: ConditionalRule, values: FormValues): boolean { + const op = rule.operator as ConditionalOperator + const actual = values[rule.field_slug] + const expected = rule.value + + switch (op) { + case 'empty': + return isEmptyValue(actual) + case 'not_empty': + return !isEmptyValue(actual) + case 'equals': + if (isEmptyValue(actual) && !isEmptyValue(expected)) return false + + return toComparable(actual) === toComparable(expected) + case 'not_equals': + if (isEmptyValue(actual) && !isEmptyValue(expected)) return true + + return toComparable(actual) !== toComparable(expected) + case 'contains': { + if (isEmptyValue(actual)) return false + if (Array.isArray(actual)) return actual.map(toComparable).includes(toComparable(expected)) + + return String(actual).includes(String(expected ?? '')) + } + case 'not_contains': { + if (isEmptyValue(actual)) return true + if (Array.isArray(actual)) return !actual.map(toComparable).includes(toComparable(expected)) + + return !String(actual).includes(String(expected ?? '')) + } + case 'in': { + if (isEmptyValue(actual)) return false + if (!Array.isArray(expected)) return false + const exp = expected.map(toComparable) + if (Array.isArray(actual)) return actual.some(a => exp.includes(toComparable(a))) + + return exp.includes(toComparable(actual)) + } + case 'not_in': { + if (isEmptyValue(actual)) return true + if (!Array.isArray(expected)) return true + const exp = expected.map(toComparable) + if (Array.isArray(actual)) return !actual.some(a => exp.includes(toComparable(a))) + + return !exp.includes(toComparable(actual)) + } + case 'greater_than': { + if (isEmptyValue(actual)) return false + const a = Number(actual) + const e = Number(expected) + if (Number.isNaN(a) || Number.isNaN(e)) return false + + return a > e + } + case 'less_than': { + if (isEmptyValue(actual)) return false + const a = Number(actual) + const e = Number(expected) + if (Number.isNaN(a) || Number.isNaN(e)) return false + + return a < e + } + default: + return true + } +} + +function isGroup(node: ConditionalRule | ConditionalGroup): node is ConditionalGroup { + return typeof node === 'object' && node !== null && (('all' in node) || ('any' in node)) +} + +function evaluateGroup(group: ConditionalGroup, values: FormValues): boolean { + if (Array.isArray(group.all) && group.all.length > 0) { + for (const node of group.all) { + const ok = isGroup(node) ? evaluateGroup(node, values) : evaluateRule(node, values) + if (!ok) return false + } + + return true + } + if (Array.isArray(group.any) && group.any.length > 0) { + for (const node of group.any) { + const ok = isGroup(node) ? evaluateGroup(node, values) : evaluateRule(node, values) + if (ok) return true + } + + return false + } + + return true +} + +/** + * Evaluate a conditional logic block against the current form values. + * Returns true when the field/group should be visible; defaults to true + * when the logic is absent or malformed. + */ +export function evaluateConditionalLogic( + logic: ConditionalLogic | null | undefined, + values: FormValues, +): boolean { + if (!logic || !logic.show_when) return true + + return evaluateGroup(logic.show_when, values) +} diff --git a/apps/portal/src/composables/useFormSteps.ts b/apps/portal/src/composables/useFormSteps.ts new file mode 100644 index 00000000..fdace76d --- /dev/null +++ b/apps/portal/src/composables/useFormSteps.ts @@ -0,0 +1,138 @@ +import { computed } from 'vue' +import type { ComputedRef, Ref } from 'vue' +import { evaluateConditionalLogic } from '@/composables/useConditionalLogic' +import { FormFieldType } from '@/types/formBuilder' +import type { FormValues, PublicFormField, PublicFormSchema } from '@/types/formBuilder' +import { isFieldValueEmpty } from '@/utils/formValidation' + +export type StepKind = 'submitter' | 'section' | 'heading_group' | 'flat' | 'review' + +export interface FormStep { + key: string + kind: StepKind + title: string + subtitle?: string + fields: PublicFormField[] +} + +function partitionByHeading(fields: PublicFormField[]): FormStep[] { + if (fields.length === 0) return [] + + const out: FormStep[] = [] + let current: FormStep | null = null + let index = 0 + + for (const field of fields) { + if (field.field_type === FormFieldType.HEADING) { + current = { + key: `heading-${field.id}`, + kind: 'heading_group', + title: field.label, + fields: [field], + } + out.push(current) + index++ + continue + } + if (!current) { + current = { + key: `group-${index}`, + kind: 'heading_group', + title: 'Vragen', + fields: [], + } + out.push(current) + index++ + } + current.fields.push(field) + } + + return out +} + +export function useFormSteps(schema: Ref): ComputedRef { + return computed(() => { + const s = schema.value + const submitterStep: FormStep = { + key: 'submitter', + kind: 'submitter', + title: 'Contactgegevens', + subtitle: 'Zo kunnen we contact met je opnemen', + fields: [], + } + const reviewStep: FormStep = { + key: 'review', + kind: 'review', + title: 'Controleer en versturen', + subtitle: 'Check je antwoorden en verstuur het formulier', + fields: [], + } + + if (!s) return [submitterStep, reviewStep] + + const sorted = [...s.fields].sort((a, b) => a.sort_order - b.sort_order) + + const steps: FormStep[] = [submitterStep] + + if (s.sections.length > 0 && s.section_level_submit === false) { + const sectionsSorted = [...s.sections].sort((a, b) => a.sort_order - b.sort_order) + for (const section of sectionsSorted) { + const fields = sorted.filter(f => f.form_schema_section_id === section.id) + steps.push({ + key: `section-${section.id}`, + kind: 'section', + title: section.name, + subtitle: section.description ?? undefined, + fields, + }) + } + const loose = sorted.filter(f => f.form_schema_section_id === null) + if (loose.length > 0) { + steps.push({ + key: 'section-loose', + kind: 'section', + title: 'Overig', + fields: loose, + }) + } + } + else if (sorted.some(f => f.field_type === FormFieldType.HEADING)) { + steps.push(...partitionByHeading(sorted)) + } + else { + steps.push({ + key: 'all-fields', + kind: 'flat', + title: 'Vragen', + fields: sorted, + }) + } + + steps.push(reviewStep) + + return steps + }) +} + +/** + * Returns true when all visible required fields in `step` have a + * non-empty value. Hidden fields (failing conditional logic) are + * skipped. HEADING/PARAGRAPH fields carry no value and are skipped. + */ +export function isStepValid( + step: FormStep, + values: FormValues, + submitterValid: boolean, +): boolean { + if (step.kind === 'submitter') return submitterValid + if (step.kind === 'review') return true + + for (const field of step.fields) { + if (field.field_type === FormFieldType.HEADING || field.field_type === FormFieldType.PARAGRAPH) continue + if (!field.is_required) continue + if (!evaluateConditionalLogic(field.conditional_logic, values)) continue + if (isFieldValueEmpty(values[field.slug])) return false + } + + return true +} diff --git a/apps/portal/src/pages/register/[eventSlug].vue b/apps/portal/src/pages/register/[eventSlug].vue deleted file mode 100644 index 40bfecac..00000000 --- a/apps/portal/src/pages/register/[eventSlug].vue +++ /dev/null @@ -1,1537 +0,0 @@ - - - - - diff --git a/apps/portal/src/utils/formValidation.ts b/apps/portal/src/utils/formValidation.ts new file mode 100644 index 00000000..a17fe830 --- /dev/null +++ b/apps/portal/src/utils/formValidation.ts @@ -0,0 +1,149 @@ +import { emailValidator, regexValidator, requiredValidator, urlValidator } from '@core/utils/validators' +import { FormFieldType } from '@/types/formBuilder' +import type { PublicFormField } from '@/types/formBuilder' + +export type Validator = (value: unknown) => true | string + +/** + * Build a list of client-side validators for a public form field. + * Runs in VTextField `:rules` and the stepper "current step valid" + * gate. Mirrors (a subset of) the backend relaxed rule set so the + * submitter gets feedback before the submit round-trip. + */ +export function getValidatorsForField(field: PublicFormField): Validator[] { + const rules: Validator[] = [] + const v = field.validation_rules ?? {} + + if (field.is_required) { + rules.push(value => { + const ok = requiredValidator(value) + + return ok === true ? true : 'Dit veld is verplicht.' + }) + } + + switch (field.field_type) { + case FormFieldType.EMAIL: + rules.push(value => { + const ok = emailValidator(value) + + return ok === true ? true : 'Vul een geldig e-mailadres in.' + }) + break + + case FormFieldType.URL: + rules.push(value => { + const ok = urlValidator(value) + + return ok === true ? true : 'Vul een geldige URL in (beginnend met http:// of https://).' + }) + break + + case FormFieldType.PHONE: + rules.push(value => { + if (value === null || value === undefined || value === '') return true + const s = String(value).replace(/\s+/g, '') + + return /^\+?[\d()-]{6,}$/.test(s) || 'Vul een geldig telefoonnummer in.' + }) + break + + case FormFieldType.NUMBER: + rules.push(value => { + if (value === null || value === undefined || value === '') return true + const n = Number(value) + + return Number.isFinite(n) || 'Vul een geldig getal in.' + }) + if (typeof v.min === 'number') { + const min = v.min + rules.push(value => { + if (value === null || value === undefined || value === '') return true + const n = Number(value) + + return Number.isFinite(n) && n >= min ? true : `Minimaal ${min}.` + }) + } + if (typeof v.max === 'number') { + const max = v.max + rules.push(value => { + if (value === null || value === undefined || value === '') return true + const n = Number(value) + + return Number.isFinite(n) && n <= max ? true : `Maximaal ${max}.` + }) + } + break + + case FormFieldType.TEXT: + case FormFieldType.TEXTAREA: + if (typeof v.min === 'number') { + const min = v.min + rules.push(value => { + if (value === null || value === undefined || value === '') return true + + return String(value).length >= min ? true : `Minimaal ${min} tekens.` + }) + } + if (typeof v.max === 'number') { + const max = v.max + rules.push(value => { + if (value === null || value === undefined || value === '') return true + + return String(value).length <= max ? true : `Maximaal ${max} tekens.` + }) + } + if (typeof v.pattern === 'string' && v.pattern.length > 0) { + const pattern = v.pattern + rules.push(value => { + if (value === null || value === undefined || value === '') return true + const ok = regexValidator(value, pattern) + + return ok === true ? true : 'Ongeldige invoer.' + }) + } + break + + case FormFieldType.MULTISELECT: + case FormFieldType.CHECKBOX_LIST: + if (typeof v.min_selections === 'number') { + const min = v.min_selections + rules.push(value => { + const arr = Array.isArray(value) ? value : [] + + return arr.length >= min ? true : `Kies er minimaal ${min}.` + }) + } + if (typeof v.max_selections === 'number') { + const max = v.max_selections + rules.push(value => { + const arr = Array.isArray(value) ? value : [] + + return arr.length <= max ? true : `Kies er maximaal ${max}.` + }) + } + break + + default: + break + } + + return rules +} + +export function runValidators(rules: Validator[], value: unknown): string | true { + for (const rule of rules) { + const r = rule(value) + if (r !== true) return r + } + + return true +} + +export function isFieldValueEmpty(value: unknown): boolean { + if (value === null || value === undefined) return true + if (typeof value === 'string') return value.trim() === '' + if (Array.isArray(value)) return value.length === 0 + + return false +} diff --git a/apps/portal/typed-router.d.ts b/apps/portal/typed-router.d.ts index a89a2e1b..e3b122e5 100644 --- a/apps/portal/typed-router.d.ts +++ b/apps/portal/typed-router.d.ts @@ -25,7 +25,7 @@ declare module 'vue-router/auto-routes' { 'portal-event-detail': RouteRecordInfo<'portal-event-detail', '/evenementen/:eventId', { eventId: ParamValue }, { eventId: ParamValue }>, 'login': RouteRecordInfo<'login', '/login', Record, Record>, 'portal-profiel': RouteRecordInfo<'portal-profiel', '/profiel', Record, Record>, - 'volunteer-register': RouteRecordInfo<'volunteer-register', '/register/:eventSlug', { eventSlug: ParamValue }, { eventSlug: ParamValue }>, + 'public-form-register': RouteRecordInfo<'public-form-register', '/register/:public_token', { public_token: ParamValue }, { public_token: ParamValue }>, 'register-success': RouteRecordInfo<'register-success', '/register/success', Record, Record>, 'volunteer-register-info': RouteRecordInfo<'volunteer-register-info', '/registreren', Record, Record>, 'portal-shifts': RouteRecordInfo<'portal-shifts', '/shifts', Record, Record>,