├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── LICENSE ├── PUBLISH_STEPS.md ├── README.md ├── package.json ├── playwright.config.ts ├── src ├── app.d.ts ├── app.html ├── lib │ ├── chromeAutofill.ts │ ├── components │ │ ├── Hint.svelte │ │ └── HintGroup.svelte │ ├── index.ts │ ├── models │ │ ├── form.ts │ │ ├── formControl.ts │ │ ├── formControlElement.ts │ │ ├── formProperties.ts │ │ └── validator.ts │ ├── stores │ │ └── formReferences.ts │ ├── useForm.ts │ ├── validators.ts │ └── validatorsAction.ts └── routes │ ├── +layout.svelte │ ├── +page.svelte │ ├── NavigationBar.svelte │ ├── examples │ ├── ReuseFormDialog.svelte │ ├── async │ │ └── +page.svelte │ ├── custom-validator │ │ └── +page.svelte │ ├── dynamic-form │ │ └── +page.svelte │ ├── dynamic-typing │ │ └── +page.svelte │ ├── dynamic-validator │ │ └── +page.svelte │ ├── email-validator │ │ └── +page.svelte │ ├── ignore-attribute │ │ └── +page.svelte │ ├── login │ │ └── +page.svelte │ ├── multi-forms │ │ └── +page.svelte │ ├── reset-form │ │ └── +page.svelte │ └── reuse-form │ │ └── +page.svelte │ └── test │ └── hint │ └── +page.svelte ├── static └── favicon.png ├── svelte-use-form.svg ├── svelte.config.js ├── tests ├── Hint.spec.ts ├── dynamic-validators.spec.ts ├── email-validator.spec.ts ├── ignore.spec.ts ├── login.spec.ts └── multiforms.spec.ts ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | 6 | jobs: 7 | e2e: 8 | name: Run end-to-end tests 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - uses: actions/setup-node@v3 13 | - name: Install dependencies 14 | run: yarn install --frozen-lockfile 15 | - name: Build App 16 | run: yarn build 17 | - name: Install playwright browsers 18 | run: npx playwright install --with-deps 19 | - name: Run tests 20 | run: npx playwright test --reporter github 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /.svelte-kit 4 | /package 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-svelte"], 3 | "pluginSearchDirs": ["."], 4 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.editor.labelFormat": "short" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Noah Versace Salvi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /PUBLISH_STEPS.md: -------------------------------------------------------------------------------- 1 | #  Workflow to publish to npm 2 | 3 | - Run `npm version [patch/minor/major]`. This will create a commit and bump the version in `package.json`. 4 | - Run `yarn package` to bulid the project as a package. 5 | - Run `cd package` and then `npm publish` to publish it to the registry. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |   4 | svelte-use-form 5 |

6 | 7 | A Svelte form library that enables you to create complicated forms with minimal effort. As for Svelte, the focus is **DX** 💻‍✨ 8 | 9 | ```bash 10 | npm i -D svelte-use-form 11 | ``` 12 | 13 | ![GitHub package.json version](https://img.shields.io/github/package-json/v/noahsalvi/svelte-use-form?style=for-the-badge) 14 | ![npm](https://img.shields.io/npm/dw/svelte-use-form?style=for-the-badge) 15 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg?style=for-the-badge)](https://paypal.me/noahvsalvi) 16 | 17 | **Features:** 18 | 19 | - Minimalistic - Don't write more than necessary. 20 | - No new components, bindings or callbacks required 21 | - Validators included and custom validator support 22 | - Automagically binds to inputs 23 | 24 | # Usage 25 | 26 | It's pretty self-explanatory… check out the examples below 😉 27 | 28 | Just make sure to prefix the form with `$`, when accessing its state. 29 | 30 | **Minimal Example** [REPL](https://svelte.dev/repl/faf5a9ab763640ed830028c970421f72?version=3.35.0) 31 | 32 | ```svelte 33 | 38 | 39 |
40 | 41 | 42 | The title requires at least {value} characters. 43 | 44 | 45 |
46 |
47 | ``` 48 | 49 | or you could also print the error message like this: 50 | 51 | ```svelte 52 | ... 53 | {#if $form.title?.touched && $form.title?.errors.minLength} 54 | The title requires at least {$form.title.errors.minLength} characters. 55 | {/if} 56 | ``` 57 | 58 | ## More Examples 59 | 60 |
61 | 62 | 63 | Login Example 64 | 65 | REPL 66 | 67 | 68 | 69 | 70 | ```svelte 71 | 83 | 84 |
85 |

Login

86 | 87 | 88 | 89 | This is a mandatory field 90 | Email is not valid 91 | 92 | 93 | 94 | This is a mandatory field 95 | 96 | 97 |
98 | ``` 99 | 100 |
101 | 102 |
103 | 104 | 105 | Registration Example 106 | 107 | REPL 108 | 109 | 110 | 111 | 112 | ```svelte 113 | 141 | 142 |
143 |
144 |

Registration

145 | 146 | 147 | 148 | {requiredMessage} 149 | This must be a valid email 150 | 151 | 152 | 153 | 154 | 155 | 156 | 161 | 162 | {requiredMessage} 163 | This field must have at least {value} characters. 166 | 167 | This field must contain at least {value} numbers. 168 | 169 | 170 | 171 | 172 | 177 | 178 | {requiredMessage} 179 | Passwords do not match 180 |
181 | 182 | 183 |
184 |
185 | 		{JSON.stringify($form, null, 1)}
186 | 	
187 |
188 | 189 | 206 | ``` 207 | 208 |
209 | 210 | **[Edge Cases REPL](https://svelte.dev/repl/d4fc021f688d4ad0b3ceb9a1c44c9be9?version=3.34.0)** 211 | 212 | # API 213 | 214 | ## `useForm(properties: FormProperties?, formName?: string)` 215 | 216 | useForm() returns a svelte `store` (Observable) that is also an `action`. (That's what I call [svelte](https://www.dictionary.com/browse/svelte) 😆) 217 | 218 | ### Why specify `FormProperties`? 219 | 220 | Providing the names of the properties as arguments allows us to initialize all form controls in the form before the site is actually rendered. Thus you won't need to null-check them when accessing them. 221 | 222 | ```svelte 223 | const form = useForm({ firstName: {} }); 224 | $form.firstName // Works as expected 225 | $form?.lastName // lastName would be null on page load 226 | ``` 227 | 228 | ### Why specify `formName`? 229 | 230 | By providing a name you'll have the ability to specifically reference a form from a `Hint` component instead of inferring it from context. 231 | 232 | `useForm({}, "form-1")` --> ``. 233 | 234 | This allows you to use multiple `useForm` instances in a single component. 235 | 236 | ### `$form` 237 | 238 | Subscribe to the form with `$`-prefix to access the state of the form. It returns a `Form` instance. 239 | 240 | ### `Form` 241 | 242 | **Remark**: In reality the "Form" is an union of multiple types and its self. 243 | 244 | ```typescript 245 | class Form { 246 | valid: boolean; 247 | touched: boolean; 248 | values: { 249 | [controlName: string]: string; 250 | }; 251 | reset(): void; 252 | [controlName: string]: FormControl | undefined; 253 | } 254 | ``` 255 | 256 | ### `FormProperties` (Optional) 257 | 258 | ```typescript 259 | export type FormProperties = { 260 | [key: string]: { 261 | /** Initial value of the form control */ 262 | initial?: string; 263 | /** The validators that will be checked when the input changes */ 264 | validators?: Validator[]; 265 | /** 266 | * The map through which validation errors will be mapped. 267 | * You can either pass a string or a function returning a new error value 268 | * 269 | * **Think of it as a translation map. 😆** 270 | */ 271 | errorMap?: ErrorMap; 272 | }; 273 | }; 274 | ``` 275 | 276 | ## `FormControl` 277 | 278 | A FormControl represents an input of the form. (input, textarea, radio, select...) 279 | 280 | **Important**: 281 | Every control in the form will be accessible through `$form` directly via the `name` attribute. 282 | 283 | e.g. `` --> `$form.email` 284 | 285 | ```typescript 286 | class FormControl { 287 | value: string; 288 | touched: boolean; 289 | validators: Validator[]; 290 | /** Does the FormControl pass all given validators? */ 291 | valid: boolean; 292 | /** The initial value of the FormControl. */ 293 | initial: string; 294 | /** The DOM elements representing this control*/ 295 | elements: FormControlElement[]; 296 | 297 | /** Returns an object containing possible validation errors */ 298 | errors: ValidationErrors; 299 | /** 300 | * Contains a map of values, that will be shown 301 | * in place of the original validation error. 302 | */ 303 | errorMap: ErrorMap; 304 | 305 | error(errors: ValidationErrors): void; 306 | /** Change the value and the value of all HTML-Elements associated with this control */ 307 | change(value: any): void; 308 | /** Validate the FormControl by querying through the given validators. */ 309 | validate(): boolean; 310 | /** Reset the form control value to its initial value or `{ value }` and untouch it */ 311 | reset({ value }?: { value?: string | null }): void; 312 | } 313 | ``` 314 | 315 | ## `use:validators` (Action) 316 | 317 | Takes in the validators that should be used on the form control. 318 | e.g. 319 | 320 | ```svelte 321 | 322 | ``` 323 | 324 | ## `` 325 | 326 | Helper component for displaying information based on errors in an input. 327 | 328 | **Properties:** 329 | 330 | - `for="name_of_input"` - Name of concerning input 331 | - `on="error"` - The error which should trigger it 332 | 333 | **Optional attributes:** 334 | 335 | - `form="form-name"` - Name of the form. Defaults to form from context. 336 | - `hideWhen="different_error"` - Hides the hint if the error is active 337 | - `hideWhenRequired` - Shortcut for hideWhen="required" 338 | - `showWhenUntouched` - Display Hint even if the field hasn't been touched yet 339 | - `let:value` - Returns the value of the error 340 | 341 | ## `` 342 | 343 | You can omit the Hint `for` property when wrapping it with a `HintGroup`. 344 | 345 | **Properties:** 346 | 347 | - `for="name_of_input"` 348 | 349 | **Optional Properties:** 350 | 351 | - `form="form-name"` - Name of the form. Defaults to form from context. 352 | 353 | ## Ignore a form control 354 | 355 | You can ignore a form control element by adding the `data-suf-ignore` attribute to it. 356 | 357 | ```svelte 358 | 359 | ``` 360 | 361 | This will prevent it from being added to the form elements. 362 | And thus it won't be validated or observed for changes. 363 | 364 | ## Validators 365 | 366 | - `required` 367 | - `minLength(n)` 368 | - `maxLength(n)` 369 | - `number` 370 | - `email` 371 | - `emailWithTld` 372 | - `url` 373 | - `pattern(regExp)` 374 | 375 | ### Custom Validator 376 | 377 | A validator needs to be a function that returns null if valid else an object with the key being the name of the error. The value of the object will be accessible through the error. e.g. $form.title.errors.name_of_error -> 'info'. 378 | 379 | ```typescript 380 | const passwordMatch: Validator = (value, form) => { 381 | return value === form.password?.value 382 | ? null 383 | : { passwordMatch: "Passwords are not matching" }; 384 | }; 385 | ``` 386 | 387 | ``` 388 | ... use:validators={[passwordMatch]} 389 | or 390 | ... passwordConfirmation: { validators: [passwordMatch] } } 391 | 392 | ... $form.title.errors.passwordMatch 393 | ``` 394 | 395 | An example with [validator.js](https://www.npmjs.com/package/validator) [REPL](https://svelte.dev/repl/21fc7637645d4917994ad4140b54b871?version=3.35.0) 396 | 397 | # Remarks 398 | 399 | ## Chrome Autofill Solution 400 | 401 | When Chrome autofills the form on page load, it will always register all inputs as valid. After clicking anywhere on the site, pressing a key or pressing the submit button it will then reevaluate all fields and update the state of the form. 402 | 403 | This solution was needed due to Chrome's way of autofilling forms without actually setting the value of the inputs until the page gets a click or key event. 404 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-use-form", 3 | "version": "2.10.0", 4 | "description": "The most svelte form library! 🥷", 5 | "author": "Noah Versace Salvi", 6 | "homepage": "https://github.com/noahsalvi/svelte-use-form#readme", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/noahsalvi/svelte-use-form.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/noahsalvi/svelte-use-form/issues" 14 | }, 15 | "keywords": [ 16 | "svelte", 17 | "form", 18 | "validator", 19 | "validation", 20 | "hint", 21 | "action" 22 | ], 23 | "scripts": { 24 | "dev": "vite dev", 25 | "build": "vite build", 26 | "package": "svelte-kit sync && svelte-package", 27 | "preview": "vite preview", 28 | "test": "npx playwright test", 29 | "prepublishOnly": "echo 'Did you mean to publish `./package/`, instead of `./`?' && exit 1", 30 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", 31 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", 32 | "lint": "prettier --plugin-search-dir . --check .", 33 | "format": "prettier --plugin-search-dir . --write ." 34 | }, 35 | "peerDependencies": { 36 | "svelte": ">=3.46.0 < 5" 37 | }, 38 | "devDependencies": { 39 | "@playwright/test": "^1.36.1", 40 | "@sveltejs/adapter-auto": "next", 41 | "@sveltejs/kit": "next", 42 | "@sveltejs/package": "^1.0.0-next.6", 43 | "prettier": "^2.7.1", 44 | "prettier-plugin-svelte": "^2.8.1", 45 | "svelte": "^3.46.0", 46 | "svelte-check": "^2.7.1", 47 | "svelte-preprocess": "^4.10.6", 48 | "tslib": "^2.3.1", 49 | "typescript": "^4.7.4", 50 | "vite": "^3.1.0" 51 | }, 52 | "type": "module" 53 | } 54 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from "@playwright/test"; 2 | 3 | // Run 'npm start build' before running the tests 4 | 5 | const config: PlaywrightTestConfig = { 6 | webServer: { 7 | command: "npx vite preview --port 4175", 8 | port: 4175, 9 | reuseExistingServer: !process.env.CI, 10 | }, 11 | }; 12 | 13 | export default config; 14 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | // See https://kit.svelte.dev/docs/types#app 4 | // for information about these interfaces 5 | // and what to do when importing types 6 | declare namespace App { 7 | // interface Locals {} 8 | // interface PageData {} 9 | // interface Error {} 10 | // interface Platform {} 11 | } 12 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %sveltekit.head% 8 | 9 | 10 | 11 |
%sveltekit.body%
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/lib/chromeAutofill.ts: -------------------------------------------------------------------------------- 1 | import type { FormControl } from "./models/formControl"; 2 | import type { TextElement } from "./models/formControlElement"; 3 | 4 | const isChrome = () => 5 | /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor); 6 | const animationName = "svelte-use-form-webkit-autofill"; 7 | const css = ` 8 | @keyframes ${animationName} { 9 | from {} 10 | } 11 | 12 | input:-webkit-autofill { 13 | animation-name: svelte-use-form-webkit-autofill; 14 | } 15 | `; 16 | 17 | function startAnimationWhenAutofilled() { 18 | if ( 19 | document.getElementById("svelte-use-form-chrome-autofill-styles") === null 20 | ) { 21 | const style = document.createElement("style"); 22 | style.setAttribute("id", "svelte-use-form-chrome-autofill-styles"); 23 | style.setAttribute("type", "text/css"); 24 | style.appendChild(document.createTextNode(css)); 25 | document.head.appendChild(style); 26 | } 27 | } 28 | 29 | export function handleChromeAutofill( 30 | textElement: TextElement, 31 | control: FormControl, 32 | callback: Function 33 | ) { 34 | if (!isChrome()) return; 35 | 36 | function handleAnimationStart(event: AnimationEvent) { 37 | if (event.animationName.includes(animationName)) { 38 | const currentValue = textElement.value; 39 | // If chrome did not actually fill the value of the input 40 | if (!currentValue) { 41 | control.valid = true; 42 | callback(); 43 | } 44 | } 45 | } 46 | 47 | textElement.addEventListener("animationstart", handleAnimationStart); 48 | startAnimationWhenAutofilled(); 49 | } 50 | -------------------------------------------------------------------------------- /src/lib/components/Hint.svelte: -------------------------------------------------------------------------------- 1 | 55 | 56 | {#if !(hideWhenRequired && requiredError) && !hideWhenError} 57 | {#if (touched || showWhenUntouched) && value} 58 |
59 | 60 |
61 | {/if} 62 | {/if} 63 | -------------------------------------------------------------------------------- /src/lib/components/HintGroup.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Hint } from "./components/Hint.svelte"; 2 | export { default as HintGroup } from "./components/HintGroup.svelte"; 3 | export { useForm } from "./useForm"; 4 | export { validators, ValidatorsActionError } from "./validatorsAction"; 5 | export { Form, FormControlMissingError } from "./models/form"; 6 | export { FormControl } from "./models/formControl"; 7 | export type { 8 | FormControlsSpecified, 9 | FormControlsUnspecified, 10 | } from "./models/form"; 11 | export { 12 | required, 13 | minLength, 14 | maxLength, 15 | email, 16 | emailWithTld, 17 | url, 18 | number, 19 | pattern, 20 | } from "./validators"; 21 | export type { Validator, ValidationErrors, ErrorMap } from "./models/validator"; 22 | -------------------------------------------------------------------------------- /src/lib/models/form.ts: -------------------------------------------------------------------------------- 1 | import { FormControl } from "./formControl"; 2 | import type { FormControlElement } from "./formControlElement"; 3 | import type { FormProperties } from "./formProperties"; 4 | import type { ErrorMap, Validator } from "./validator"; 5 | 6 | export class Form { 7 | /** 8 | * Function for creating a Form 9 | * @remarks This allows us to specify the index signatures in the class 10 | */ 11 | static create( 12 | initialData: FormProperties, 13 | notifyListeners: Function 14 | ) { 15 | return new Form(initialData, notifyListeners) as Form & 16 | FormControlsSpecified & 17 | FormControlsUnspecified; 18 | } 19 | private readonly _notifyListeners: Function; 20 | 21 | get valid(): boolean { 22 | let valid = true; 23 | this.forEachControl((formControl) => { 24 | if (!formControl.valid) valid = false; 25 | }); 26 | return valid; 27 | } 28 | 29 | get touched(): boolean { 30 | let touched = true; 31 | this.forEachControl((formControl) => { 32 | if (!formControl.touched) touched = false; 33 | }); 34 | return touched; 35 | } 36 | 37 | get values(): FormValues { 38 | let values = {} as any; 39 | this.forEachControl((formControl, key) => { 40 | values[key] = formControl.value; 41 | }); 42 | 43 | return values; 44 | } 45 | 46 | set touched(value: boolean) { 47 | this.forEachControl((formControl) => { 48 | formControl.touched = value; 49 | }); 50 | 51 | this._notifyListeners(); 52 | } 53 | 54 | private constructor(initialData: FormProperties, notifyListeners: Function) { 55 | for (const [k, v] of Object.entries(initialData)) { 56 | this._addControl(k, v.initial, v.validators, [], v.errorMap); 57 | } 58 | 59 | this._notifyListeners = notifyListeners; 60 | } 61 | 62 | /** Reset the whole form */ 63 | reset() { 64 | this.forEachControl((formControl) => formControl.reset()); 65 | } 66 | 67 | /** @internal Add a form conrol to the Form */ 68 | _addControl( 69 | name: string, 70 | initial: string = "", 71 | validators: Validator[] = [], 72 | elements: FormControlElement[] = [], 73 | errorMap: ErrorMap = {} 74 | ) { 75 | (this as any)[name] = new FormControl({ 76 | value: initial, 77 | validators: validators, 78 | elements: elements, 79 | errorMap: errorMap, 80 | formRef: () => this, 81 | }); 82 | } 83 | 84 | private forEachControl( 85 | callback: (formControl: FormControl, key: string) => void 86 | ) { 87 | for (const [key, value] of Object.entries(this)) { 88 | if (value instanceof FormControl) { 89 | callback(value, key); 90 | } 91 | } 92 | } 93 | } 94 | 95 | export class FormControlMissingError extends Error {} 96 | 97 | // We do not use utility types here, since they would hide the name of the type 98 | export type FormControlsUnspecified = { 99 | [key: string]: FormControl | undefined; 100 | }; 101 | export type FormControlsSpecified = { 102 | [K in Keys]: FormControl; 103 | }; 104 | export type FormValues = Partial< 105 | Record 106 | > & 107 | Record; 108 | -------------------------------------------------------------------------------- /src/lib/models/formControl.ts: -------------------------------------------------------------------------------- 1 | import type { Form } from "./form"; 2 | import type { FormControlElement } from "./formControlElement"; 3 | import type { Validator, ValidationErrors, ErrorMap } from "./validator"; 4 | 5 | /** A FormControl represents the state of a {@link FormControlElement} like (input, textarea...) */ 6 | export class FormControl { 7 | validators: Validator[]; 8 | 9 | /** 10 | * Returns an object containing possible validation errors 11 | * @example 12 | * (All validators are throwing an error) 13 | * `{ required: true, minLength: 4, maxLength: 20 }` 14 | * (Only required is invalid) 15 | * `{ required: true }` 16 | */ 17 | errors: ValidationErrors = {}; 18 | 19 | /** 20 | * Contains a map of values, that will be shown 21 | * in place of the original validation error. 22 | */ 23 | errorMap: ErrorMap = {}; 24 | 25 | /** 26 | * The DOM elements representing this control 27 | */ 28 | elements: FormControlElement[] = []; 29 | 30 | /** Does the FormControl pass all given validators? */ 31 | valid = true; 32 | 33 | /** 34 | * If the FormControl has been interacted with. 35 | * (triggered by blur event) 36 | */ 37 | _touched = false; 38 | 39 | /** The initial value of the FormControl. Defaults to `""` if not set via `useForm(params)`. */ 40 | initial: string; 41 | 42 | // TODO can we get the Form via Svelte context? 43 | private readonly formRef: () => Form; 44 | 45 | private _value: string = ""; 46 | 47 | get value() { 48 | return this._value; 49 | } 50 | 51 | get touched() { 52 | return this._touched; 53 | } 54 | 55 | /** 56 | * This will only change the internal value of the control, not the one displayed in the actual HTML-Element 57 | * 58 | * See `change(value: String)` for doing both 59 | */ 60 | set value(value: string) { 61 | this._value = value; 62 | this.validate(); 63 | } 64 | 65 | set touched(value: boolean) { 66 | this._touched = value; 67 | this.elements.forEach((element) => { 68 | if (value) element.classList.add("touched"); 69 | else element.classList.remove("touched"); 70 | }); 71 | } 72 | 73 | constructor(formControl: { 74 | value: string; 75 | validators: Validator[]; 76 | errorMap: ErrorMap; 77 | elements: FormControlElement[]; 78 | formRef: () => Form; 79 | }) { 80 | this.formRef = formControl.formRef; 81 | this.validators = formControl.validators; 82 | this.errorMap = formControl.errorMap; 83 | this.initial = formControl.value; 84 | this.elements = formControl.elements; 85 | this.value = formControl.value; 86 | } 87 | 88 | /** 89 | * Set an error manually. 90 | * 91 | * The error will be removed after changes to the value or on validate() 92 | * 93 | * Used for setting an error that would be difficult to implement with a validator. 94 | * @example Backend Response returning Login failed 95 | * ``` typescript 96 | * function submit() { 97 | * apiLogin($form.values).then(response => {}) 98 | * .catch(error => { 99 | * if (error.status === 403) { 100 | * $form.password.error({ login: "Password or username is incorrect" }); 101 | * } 102 | * }) 103 | * } 104 | * ``` 105 | */ 106 | error(errors: ValidationErrors) { 107 | this.errors = { ...errors, ...this.errors }; 108 | this.valid = false; 109 | 110 | // Update the $form 111 | this.formRef()["_notifyListeners"](); 112 | } 113 | 114 | /** Change the value and the value of all HTML-Elements associated with this control */ 115 | change(value: any) { 116 | this.value = value; 117 | this.elements.forEach((element) => (element.value = value)); 118 | 119 | // Update the $form 120 | this.formRef()["_notifyListeners"](); 121 | } 122 | 123 | /** Validate the FormControl by querying through the given validators. */ 124 | validate() { 125 | let valid = true; 126 | this.errors = {}; 127 | 128 | for (const validator of this.validators) { 129 | const errors = validator(this.value, this.formRef() as any, this); 130 | if (!errors) continue; 131 | 132 | valid = false; 133 | for (const error of Object.entries(errors)) { 134 | let [key, value] = error; 135 | 136 | // If there is a map for the error, use it 137 | const errorMapping = this.errorMap[key]; 138 | if (errorMapping) { 139 | value = 140 | typeof errorMapping === "function" 141 | ? errorMapping(value) 142 | : errorMapping; 143 | } 144 | 145 | this.errors[key] = value; 146 | } 147 | } 148 | 149 | this.valid = valid; 150 | this.elements.forEach((element) => 151 | element.setCustomValidity(valid ? "" : "Field is invalid") 152 | ); 153 | 154 | return valid; 155 | } 156 | 157 | /** Reset the form control value to its initial value or `{ value }` and untouch it */ 158 | reset({ value }: { value?: string | null } = {}) { 159 | const resetValue = value == null ? this.initial : value; 160 | this.elements.forEach((element) => (element.value = resetValue)); 161 | this.value = resetValue; 162 | this.touched = false; 163 | 164 | // Updating the $form 165 | this.formRef()["_notifyListeners"](); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/lib/models/formControlElement.ts: -------------------------------------------------------------------------------- 1 | export function isTextElement(node: any): node is TextElement { 2 | return ( 3 | node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement 4 | ); 5 | } 6 | 7 | export function isFormControlElement(node: any): node is FormControlElement { 8 | return ( 9 | node instanceof HTMLInputElement || 10 | node instanceof HTMLTextAreaElement || 11 | node instanceof HTMLSelectElement 12 | ); 13 | } 14 | 15 | /* This function checks the node if it has an attribute `data-suf-ignore` 16 | It's used to ignore elements that should not be part of the form 17 | */ 18 | export function isIgnoredElement(node: any): boolean { 19 | return ( 20 | (node.hasAttribute("data-suf-ignore") && 21 | node.getAttribute("data-suf-ignore") === "true") || //
22 | node.getAttribute("data-suf-ignore") === true //
or
23 | ); 24 | } 25 | 26 | export type TextElement = HTMLInputElement | HTMLTextAreaElement; 27 | export type FormControlElement = TextElement | HTMLSelectElement; 28 | -------------------------------------------------------------------------------- /src/lib/models/formProperties.ts: -------------------------------------------------------------------------------- 1 | import type { ErrorMap, Validator } from "./validator"; 2 | 3 | export type FormProperties = { 4 | [key: string]: { 5 | /** Initial value of the form control */ 6 | initial?: string; 7 | /** The validators that will be checked when the input changes */ 8 | validators?: Validator[]; 9 | /** 10 | * The map through which validation errors will be mapped. 11 | * You can either pass a string or a function returning a new error value 12 | * 13 | * **Think of it as a translation map. 😆** 14 | */ 15 | errorMap?: ErrorMap; 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/models/validator.ts: -------------------------------------------------------------------------------- 1 | import type { Form, FormControlsUnspecified } from "./form"; 2 | import type { FormControl } from "./formControl"; 3 | 4 | /** 5 | * A function that depending on the control's validity either returns: 6 | * - `null | undefined` when **valid** 7 | * - {@link ValidationErrors} when **invalid**. 8 | */ 9 | export type Validator = ( 10 | /** The value of the control. */ 11 | value: string, 12 | /** The containing form. */ 13 | form: Form & FormControlsUnspecified, 14 | /** The control this validator was assigned to. */ 15 | control: FormControl 16 | ) => ValidationErrors | (null | undefined); 17 | 18 | /** An object that contains errors thrown by the validator. */ 19 | export type ValidationErrors = { [errorName: string]: any }; 20 | 21 | export type ErrorMap = { [errorName: string]: string | ((error: any) => any) }; 22 | -------------------------------------------------------------------------------- /src/lib/stores/formReferences.ts: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store"; 2 | import type { Form, FormControlsUnspecified } from "../models/form"; 3 | 4 | export type FormReference = { 5 | form: Form & FormControlsUnspecified; 6 | node: HTMLFormElement; 7 | notifyListeners: Function; 8 | }; 9 | 10 | export const formReferences = writable([]); 11 | -------------------------------------------------------------------------------- /src/lib/useForm.ts: -------------------------------------------------------------------------------- 1 | import { setContext } from "svelte"; 2 | import { handleChromeAutofill } from "./chromeAutofill"; 3 | import { Form, FormControlMissingError } from "./models/form"; 4 | import { 5 | isIgnoredElement, 6 | isFormControlElement, 7 | isTextElement, 8 | } from "./models/formControlElement"; 9 | import type { 10 | FormControlElement, 11 | TextElement, 12 | } from "./models/formControlElement"; 13 | import { formReferences } from "./stores/formReferences"; 14 | import type { FormControl } from "./models/formControl"; 15 | import type { FormProperties } from "./models/formProperties"; 16 | 17 | interface EventListener { 18 | node: HTMLElement; 19 | event: string; 20 | listener: EventListenerOrEventListenerObject; 21 | } 22 | /** Create a new form. 23 | * 24 | * You can either pass a default configuration for the form. 25 | * 26 | * ---- 27 | * ``` svelte 28 | * 33 | * 34 | * 47 | * ``` 48 | */ 49 | export function useForm< 50 | Keys extends keyof T = "", 51 | T extends FormProperties = any 52 | >( 53 | properties: T | FormProperties = {} as FormProperties, 54 | formName: string = "svelte-use-form" 55 | ) { 56 | const subscribers: Function[] = []; 57 | 58 | let eventListeners: EventListener[] = []; 59 | 60 | let state = Form.create(properties, notifyListeners); 61 | 62 | let observer: MutationObserver; 63 | 64 | action.subscribe = subscribe; 65 | action.set = set; 66 | 67 | // Passing state via context to subcomponents like Hint 68 | setContext(formName, action); 69 | 70 | /** 71 | * ### The store and action of a form. 72 | * 73 | * Use the `$` prefix to access the state of the form; 74 | */ 75 | function action(node: HTMLFormElement) { 76 | // Bootstrap form 77 | setupForm(node); 78 | 79 | // Add form reference to global internal store 80 | formReferences.update((values) => [ 81 | ...values, 82 | { node, form: state, notifyListeners }, 83 | ]); 84 | 85 | return { 86 | update: () => {}, 87 | destroy: () => { 88 | unmountEventListeners(); 89 | observer.disconnect(); 90 | }, 91 | }; 92 | } 93 | 94 | function setupForm(node: HTMLFormElement) { 95 | const inputElements = [ 96 | ...getNodeElementsByTagName(node, "input"), 97 | ]; 98 | const textareaElements = [ 99 | ...getNodeElementsByTagName(node, "textarea"), 100 | ]; 101 | const selectElements = [ 102 | ...getNodeElementsByTagName(node, "select"), 103 | ]; 104 | const textElements = [...inputElements, ...textareaElements]; 105 | 106 | setupTextElements(textElements); 107 | setupSelectElements(selectElements); 108 | hideNotRepresentedFormControls([...textElements, ...selectElements]); 109 | setupFormObserver(node); 110 | 111 | notifyListeners(); 112 | } 113 | 114 | function setupTextElements(textElements: TextElement[]) { 115 | for (const textElement of textElements) { 116 | const name = textElement.name; 117 | let formControl = state[name]; 118 | // TextElement doesn't have FormControl yet (TextElement wasn't statically provided) 119 | if (!formControl) { 120 | const initial = getInitialValueFromTextElement(textElement); 121 | state._addControl(name, initial, [], [textElement], {}); 122 | formControl = state[name]!; 123 | } else { 124 | formControl.elements.push(textElement); 125 | if ( 126 | textElement.type === "radio" && 127 | textElement instanceof HTMLInputElement && 128 | textElement.checked 129 | ) { 130 | formControl.initial = textElement.value; 131 | } 132 | } 133 | 134 | switch (textElement.type) { 135 | case "checkbox": 136 | case "radio": 137 | mountEventListener(textElement, "click", handleBlurOrClick); 138 | break; 139 | default: 140 | setInitialValue(textElement, formControl!); 141 | handleAutofill(textElement, formControl!); 142 | mountEventListener(textElement, "blur", handleBlurOrClick); 143 | } 144 | 145 | mountEventListener(textElement, "input", handleInput); 146 | } 147 | } 148 | 149 | function setupSelectElements(selectElements: HTMLSelectElement[]) { 150 | for (const selectElement of selectElements) { 151 | const name = selectElement.name; 152 | const formControl = state[name]; 153 | 154 | if (!formControl) { 155 | const initial = selectElement.value; 156 | state._addControl(name, initial, [], [selectElement], {}); 157 | } else { 158 | formControl.elements.push(selectElement); 159 | setInitialValue(selectElement, formControl); 160 | } 161 | 162 | mountEventListener(selectElement, "input", handleInput); 163 | mountEventListener(selectElement, "input", handleBlurOrClick); 164 | mountEventListener(selectElement, "blur", handleBlurOrClick); 165 | } 166 | } 167 | 168 | function setupFormObserver(form: HTMLFormElement) { 169 | observer = new MutationObserver(observeForm); 170 | observer.observe(form, { childList: true, subtree: true }); 171 | } 172 | 173 | function observeForm(mutations: MutationRecord[]) { 174 | for (const mutation of mutations) { 175 | if (mutation.type !== "childList") continue; 176 | 177 | // If node gets added 178 | for (const node of mutation.addedNodes) { 179 | if (!(isFormControlElement(node) && !isIgnoredElement(node))) continue; 180 | const initialFormControlProperty = properties[node.name]; 181 | if (!state[node.name] && initialFormControlProperty) { 182 | state._addControl( 183 | node.name, 184 | initialFormControlProperty.initial, 185 | initialFormControlProperty.validators, 186 | [], // The setup function will add this node to the form control 187 | initialFormControlProperty.errorMap 188 | ); 189 | } 190 | if (isTextElement(node)) setupTextElements([node]); 191 | else if (node instanceof HTMLSelectElement) setupSelectElements([node]); 192 | } 193 | 194 | // If node gets removed 195 | for (const node of mutation.removedNodes) { 196 | if (!(node instanceof HTMLElement)) continue; // We only handle HTML elements 197 | 198 | // The observer will only return the direct elements that were removed, and not for example a nested input 199 | const elements = isFormControlElement(node) 200 | ? [node] 201 | : getAllFormControlElements(node); 202 | 203 | elements.forEach((element) => { 204 | delete state[element.name]; 205 | eventListeners = eventListeners.filter( 206 | (eventListener) => eventListener.node !== element 207 | ); 208 | }); 209 | } 210 | } 211 | 212 | notifyListeners(); 213 | } 214 | 215 | function mountEventListener( 216 | node: HTMLElement, 217 | event: string, 218 | listener: EventListenerOrEventListenerObject 219 | ) { 220 | node.addEventListener(event, listener); 221 | eventListeners.push({ node, event, listener }); 222 | } 223 | 224 | function unmountEventListeners() { 225 | for (const { node, event, listener } of eventListeners) { 226 | node.removeEventListener(event, listener); 227 | } 228 | } 229 | 230 | function handleAutofill(textElement: TextElement, formControl: FormControl) { 231 | // Chrome sometimes fills the input visually without actually writing a value to it, this combats it 232 | handleChromeAutofill(textElement, formControl, notifyListeners); 233 | 234 | // If the browser writes a value without triggering an event 235 | function handleNoEventAutofilling() { 236 | if (textElement.value !== formControl.initial) { 237 | handleBlurOrClick({ target: textElement } as any); 238 | return true; 239 | } 240 | return false; 241 | } 242 | 243 | const autofillingWithoutEventInstantly = handleNoEventAutofilling(); 244 | 245 | // In a SPA App the form is sometimes not filled instantly so we wait 100ms 246 | if (!autofillingWithoutEventInstantly) 247 | setTimeout(() => handleNoEventAutofilling(), 100); 248 | } 249 | 250 | function handleInput({ target: node }: Event) { 251 | if (isFormControlElement(node)) { 252 | const name = node.name; 253 | const formControl = state[name]; 254 | if (!formControl) throw new FormControlMissingError(); 255 | 256 | let value: string; 257 | if (node.type === "checkbox" && node instanceof HTMLInputElement) { 258 | value = node.checked ? "checked" : ""; 259 | } else { 260 | value = node.value; 261 | } 262 | 263 | formControl.value = value; 264 | 265 | notifyListeners(); 266 | } 267 | } 268 | 269 | function handleBlurOrClick({ target: node }: Event) { 270 | if (isFormControlElement(node)) { 271 | const formControl = state[node.name]; 272 | if (!formControl) throw new FormControlMissingError(); 273 | 274 | if (!formControl.touched) handleInput({ target: node } as any); 275 | 276 | formControl.touched = true; 277 | node.classList.add("touched"); 278 | 279 | notifyListeners(); 280 | } 281 | } 282 | 283 | // TODO do we still need this? 284 | function hideNotRepresentedFormControls(nodes: FormControlElement[]) { 285 | for (const key of Object.keys(properties)) { 286 | let isFormControlRepresentedInDom = false; 287 | 288 | for (const node of nodes) { 289 | if (key === node.name) isFormControlRepresentedInDom = true; 290 | } 291 | 292 | if (!isFormControlRepresentedInDom) delete state[key]; 293 | } 294 | } 295 | 296 | function setInitialValue( 297 | element: FormControlElement, 298 | formControl: FormControl 299 | ) { 300 | if (formControl.initial) element.value = formControl.initial; 301 | } 302 | 303 | function notifyListeners() { 304 | for (const callback of subscribers) callback(state); 305 | } 306 | 307 | function subscribe(callback: (form: typeof state) => void) { 308 | subscribers.push(callback); 309 | callback(state); 310 | 311 | return { unsubscribe: () => unsubscribe(callback) }; 312 | } 313 | 314 | function unsubscribe(subscriber: Function) { 315 | const index = subscribers.indexOf(subscriber); 316 | subscribers.splice(index, 1); 317 | } 318 | 319 | function set(value: typeof state) { 320 | // TODO investigage what happens when different Keys are passed 321 | state = value; 322 | 323 | notifyListeners(); 324 | } 325 | 326 | return action; 327 | } 328 | 329 | function getInitialValueFromTextElement(textElement: TextElement) { 330 | let initial: string; 331 | 332 | // Handle Radio button initial values 333 | if (textElement.type === "radio" && textElement instanceof HTMLInputElement) { 334 | initial = textElement.checked ? textElement.value : ""; 335 | } else if ( 336 | textElement.type === "checkbox" && 337 | textElement instanceof HTMLInputElement 338 | ) { 339 | initial = textElement.checked ? "checked" : ""; 340 | } else { 341 | initial = textElement.value; 342 | } 343 | return initial; 344 | } 345 | 346 | /* 347 | Scan the DOM for a set of form elements by tag name and 348 | return the elements which are not ignored by `data-suf-ignore` attribute. 349 | */ 350 | function getNodeElementsByTagName( 351 | node: HTMLFormElement | HTMLElement, 352 | tagName: string 353 | ): T[] { 354 | return Array.from(node.getElementsByTagName(tagName)).filter( 355 | (element) => !isIgnoredElement(element) 356 | ) as T[]; 357 | } 358 | 359 | function getAllFormControlElements(node: HTMLElement): FormControlElement[] { 360 | const inputs = getNodeElementsByTagName(node, "input"); 361 | const textareas = getNodeElementsByTagName( 362 | node, 363 | "textarea" 364 | ); 365 | const selects = getNodeElementsByTagName(node, "select"); 366 | return [...inputs, ...textareas, ...selects]; 367 | } 368 | -------------------------------------------------------------------------------- /src/lib/validators.ts: -------------------------------------------------------------------------------- 1 | import type { Validator } from "./models/validator"; 2 | 3 | export const required: Validator = (value) => { 4 | return value.trim() ? null : { required: "Required" }; 5 | }; 6 | 7 | export function maxLength(length: number): Validator { 8 | return (value) => { 9 | if (value.trim().length > length) return { maxLength: length }; 10 | }; 11 | } 12 | 13 | export function minLength(length: number): Validator { 14 | return (value) => { 15 | if (value.trim().length < length) return { minLength: length }; 16 | }; 17 | } 18 | 19 | export const email: Validator = (value) => { 20 | if ( 21 | /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test( 22 | value 23 | ) 24 | ) { 25 | return null; 26 | } 27 | return { email: {} }; 28 | }; 29 | 30 | /** 31 | * A variation of the `email` validator that requires a [TLD](https://en.wikipedia.org/wiki/Top-level_domain) component. Verifying 32 | * the validity of the TLD is not the responsibility of this validation library. 33 | */ 34 | export const emailWithTld: Validator = (value) => { 35 | if ( 36 | /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/.test( 37 | value 38 | ) 39 | ) { 40 | return null; 41 | } 42 | return { emailWithTld: {} }; 43 | }; 44 | 45 | export const url: Validator = (value) => { 46 | // https://stackoverflow.com/a/5717133/13475809 47 | var pattern = new RegExp( 48 | "^(https?:\\/\\/)?" + // protocol 49 | "((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" + // domain name 50 | "((\\d{1,3}\\.){3}\\d{1,3}))" + // OR ip (v4) address 51 | "(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*" + // port and path 52 | "(\\?[;&a-z\\d%_.~+=-]*)?" + // query string 53 | "(\\#[-a-z\\d_]*)?$", // fragment locator 54 | "i" 55 | ); 56 | if (pattern.test(value)) { 57 | return null; 58 | } 59 | return { url: "URL is not valid" }; 60 | }; 61 | 62 | export const number: Validator = (value) => { 63 | if (/^[0-9]+$/.test(value)) { 64 | return null; 65 | } 66 | return { number: {} }; 67 | }; 68 | 69 | export function pattern(regExp: string | RegExp): Validator { 70 | const r = typeof regExp === "string" ? new RegExp(regExp) : regExp; 71 | return (value) => (r.test(value) ? null : { pattern: "Pattern error" }); 72 | } 73 | -------------------------------------------------------------------------------- /src/lib/validatorsAction.ts: -------------------------------------------------------------------------------- 1 | import { tick } from "svelte"; 2 | import { get } from "svelte/store"; 3 | import { FormControl } from "./models/formControl"; 4 | import type { FormControlElement } from "./models/formControlElement"; 5 | import type { Validator } from "./models/validator"; 6 | import { formReferences, type FormReference } from "./stores/formReferences"; 7 | 8 | /** 9 | * Add validators to form control 10 | * @example 11 | * ``` svelte 12 | * 13 | * ``` 14 | */ 15 | export function validators( 16 | element: FormControlElement, 17 | validators: Validator[] 18 | ) { 19 | let formControl: FormControl | undefined; 20 | let formReference: FormReference | undefined; 21 | 22 | setupValidators(); 23 | 24 | return { 25 | update: updateValidators, 26 | }; 27 | 28 | async function setupValidators() { 29 | const formElement = element.form; 30 | if (!formElement) 31 | throw new ValidatorsActionError( 32 | "HTML element doesn't have an ancestral form" 33 | ); 34 | 35 | await tick(); 36 | const possibleFormReference = get(formReferences).find( 37 | (form) => form.node === formElement 38 | ); 39 | 40 | if (!possibleFormReference) 41 | throw new ValidatorsActionError( 42 | "HTML form doesn't have a svelte-use-form binded. (use:form)" 43 | ); 44 | 45 | formReference = possibleFormReference; 46 | 47 | let possibleFormControl = formReference.form[element.name]; 48 | if (!(possibleFormControl instanceof FormControl)) 49 | throw new ValidatorsActionError( 50 | `Form Control [${element.name}] doesn't exist.` 51 | ); 52 | 53 | formControl = possibleFormControl; 54 | formControl.validators.push(...validators); 55 | formControl.validate(); 56 | formReference.notifyListeners(); 57 | } 58 | 59 | function updateValidators(updatedValidators: Validator[]) { 60 | if (!formControl || !formReference) return; 61 | 62 | // Get the static validators (The validators set via useForm({...})) 63 | const newValidators = formControl.validators.filter( 64 | (validator) => !validators.find((v) => v === validator) 65 | ); 66 | 67 | // Add the new validators to it 68 | newValidators.push(...updatedValidators); 69 | 70 | formControl.validators = newValidators; 71 | formControl.validate(); 72 | formReference.notifyListeners(); 73 | } 74 | } 75 | 76 | export class ValidatorsActionError extends Error {} 77 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 |
7 | 8 |
9 | 10 | 88 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noahsalvi/svelte-use-form/1f16ce8ed1a645ec500947d5c90090f371e0e128/src/routes/+page.svelte -------------------------------------------------------------------------------- /src/routes/NavigationBar.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 33 | 34 | 60 | -------------------------------------------------------------------------------- /src/routes/examples/ReuseFormDialog.svelte: -------------------------------------------------------------------------------- 1 | 24 | 25 | {#if showDialog} 26 |
32 |
Form
33 |

34 | 41 | Required field 42 |

43 |

44 | 50 | 51 |

52 |
60 | 61 | 62 | 71 | -------------------------------------------------------------------------------- /src/routes/examples/custom-validator/+page.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 |
22 | 23 | 29 | Input is not a valid email 30 | 31 | 32 | 38 | 39 | 40 | The password is too short, min = {value} 41 | 42 | 43 | The password is too long, max = {value} 44 | 45 | 46 | 47 | Passwords do not match 48 | 49 | 50 |
51 | -------------------------------------------------------------------------------- /src/routes/examples/dynamic-form/+page.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 13 | {#if showUsername} 14 | 15 | 20 | {/if} 21 | 24 | {#if showPassword} 25 |
26 | 27 | 32 |
33 | {/if} 34 |
35 | 36 |
37 |     {JSON.stringify($form, null, "  ")}
38 | 
39 | -------------------------------------------------------------------------------- /src/routes/examples/dynamic-typing/+page.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 | 29 | 35 | Input is not a valid email 36 | 37 | 38 | 44 | 45 | 46 | 47 | The password is too short, min = {value} 48 | 49 | 50 | The password is too long, max = {value} 51 | 52 | 53 | 54 | 55 |
56 |
57 |   {JSON.stringify($form, null, "\t")}
58 | 
59 | 
60 | -------------------------------------------------------------------------------- /src/routes/examples/dynamic-validator/+page.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | 17 | B 18 | 23 | Does B match A?{$form.copy?.valid ? "Yes" : "No"} 24 | Is required triggered? 25 | 30 |
31 |
32 |     {JSON.stringify($form.copy?.errors, null, "  ")}
33 | 
34 | 35 | 41 | -------------------------------------------------------------------------------- /src/routes/examples/email-validator/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | 13 | 18 |
19 | -------------------------------------------------------------------------------- /src/routes/examples/ignore-attribute/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | test 8 |
9 | 10 | 16 | 17 | 24 | 25 | 31 | 32 | 33 | 39 | 40 |

Registered inputs:

41 |
    42 | {#each Object.keys($form.values) as input} 43 |
  • {input}
  • 44 | {/each} 45 |
46 |
47 | -------------------------------------------------------------------------------- /src/routes/examples/login/+page.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | 17 | 23 | Input is not a valid email 24 | 25 | 26 | 32 | 33 | 34 | The password is too short, min = {value} 35 | 36 | 37 | The password is too long, max = {value} 38 | 39 | 40 | 41 | 42 |
43 | -------------------------------------------------------------------------------- /src/routes/examples/multi-forms/+page.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |

Awesome Form 1

17 | 18 | 19 | 26 | Input must be more than 3 characters 29 | Input must be less than 5 characters 32 | 33 | 38 |
39 | 40 |
41 |

Awesome Form 2

42 | 43 | 44 | 51 | 52 | 53 | 54 | Input must be more than {value} characters 55 | 56 | 57 | Input must be less than {value} characters 58 | 59 | 60 | 61 | 66 |
67 | -------------------------------------------------------------------------------- /src/routes/examples/reset-form/+page.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 | 24 | 30 | Input is not a valid email 31 | 32 | 33 | 39 | 40 | 41 | The password is too short, min = {value} 42 | 43 | 44 | The password is too long, max = {value} 45 | 46 | 47 | 48 | 49 | 50 | 51 | {#if $form.valid} 52 | Form is Valid 53 | {/if} 54 |
55 | -------------------------------------------------------------------------------- /src/routes/examples/reuse-form/+page.svelte: -------------------------------------------------------------------------------- 1 | 28 | 29 |

Svelte-use-form "reusing form" test

30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | {#each persons as p} 42 | 43 | 44 | 45 | 46 | 49 | 50 | {/each} 51 | 52 |
IDNomeAtivo 38 |
{p.id}{p.nome}{p.ativo ? "Yes" : "No"} 47 | editPerson(p)}>Edit 48 |
53 | 54 |
55 | 56 | {#key selectedPerson} 57 | 58 | {/key} 59 | 60 | 68 | -------------------------------------------------------------------------------- /src/routes/test/hint/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | 9 | Required 10 | Email 11 | Non-existent 12 |
13 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noahsalvi/svelte-use-form/1f16ce8ed1a645ec500947d5c90090f371e0e128/static/favicon.png -------------------------------------------------------------------------------- /svelte-use-form.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-auto"; 2 | import preprocess from "svelte-preprocess"; 3 | 4 | /** @type {import('@sveltejs/kit').Config} */ 5 | const config = { 6 | // Consult https://github.com/sveltejs/svelte-preprocess 7 | // for more information about preprocessors 8 | preprocess: preprocess(), 9 | 10 | kit: { 11 | adapter: adapter(), 12 | }, 13 | }; 14 | 15 | export default config; 16 | -------------------------------------------------------------------------------- /tests/Hint.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | test("Hint.svelte", async ({ page }) => { 4 | await page.goto("/test/hint"); 5 | const input = page.getByRole("textbox"); 6 | const requiredHint = page.getByText("Required"); 7 | const emailHint = page.getByText("Email"); 8 | 9 | await expect(requiredHint).not.toBeVisible(); 10 | await expect(emailHint).not.toBeVisible(); 11 | 12 | await input.focus(); 13 | await input.blur(); 14 | await expect(requiredHint).toBeVisible(); 15 | 16 | await input.type("max"); 17 | await expect(requiredHint).not.toBeVisible(); 18 | 19 | await input.type("@example.com"); 20 | await expect(emailHint).not.toBeVisible(); 21 | }); 22 | -------------------------------------------------------------------------------- /tests/dynamic-validators.spec.ts: -------------------------------------------------------------------------------- 1 | import test, { expect } from "@playwright/test"; 2 | 3 | test("Dynamic Validators (Validators with changing parameters)", async ({ 4 | page, 5 | }) => { 6 | await page.goto("examples/dynamic-validator"); 7 | 8 | const inputA = page.locator("#inputA"); 9 | const inputB = page.locator("#inputB"); 10 | const isMatching = page.locator("#is-matching"); 11 | const isRequiredErrorTriggered = page.locator("#is-empty"); 12 | 13 | expect(inputB).not.toHaveText(await inputA.inputValue()); 14 | await expect(isMatching).toHaveText("No"); 15 | 16 | await inputB.type(await inputA.inputValue()); 17 | await expect(isMatching).toHaveText("Yes"); 18 | 19 | await inputB.type("A"); 20 | await expect(isMatching).toHaveText("No"); 21 | 22 | // Focus important so that the cursor is placed at the end 23 | await inputA.focus(); 24 | await inputA.type("A"); 25 | await expect(isMatching).toHaveText("Yes"); 26 | await expect(isRequiredErrorTriggered).not.toBeChecked(); 27 | 28 | await inputB.clear(); 29 | await expect(isRequiredErrorTriggered).toBeChecked(); 30 | }); 31 | -------------------------------------------------------------------------------- /tests/email-validator.spec.ts: -------------------------------------------------------------------------------- 1 | import test, { expect } from "@playwright/test"; 2 | 3 | const validEmails = [ 4 | "email@test.com", 5 | "email@test.xyz", 6 | "email@test.a", 7 | "example.address@t.co", 8 | "example.address@a.b.c.d.e.f.g", 9 | "example_address@a.b.c", 10 | "example_address_2@a.b.c", 11 | ]; 12 | 13 | test(`Verify valid emails are marked as valid by the emailWithTld validator`, async ({ 14 | page, 15 | }) => { 16 | for (const address of validEmails) { 17 | await page.goto("examples/email-validator"); 18 | 19 | const input = page.locator("#testEmailField"); 20 | const isRequiredErrorTriggered = page.locator("#isValid"); 21 | 22 | await input.clear(); 23 | await input.type(address); 24 | await expect(isRequiredErrorTriggered).not.toBeChecked(); 25 | } 26 | }); 27 | 28 | const invalidEmails = [ 29 | "email@test", 30 | "example.address@noTLD", 31 | "example.com@a", 32 | "example.com@a@.com", 33 | "example@a@.com", 34 | "example.com@.com", 35 | "example@.com", 36 | "example.com", 37 | "example", 38 | "@example.com", 39 | ]; 40 | 41 | test(`Verify invalid emails are marked as invalid by the emailWithTld validator`, async ({ 42 | page, 43 | }) => { 44 | for (const address of invalidEmails) { 45 | await page.goto("examples/email-validator"); 46 | 47 | const input = page.locator("#testEmailField"); 48 | const isRequiredErrorTriggered = page.locator("#isValid"); 49 | 50 | await input.clear(); 51 | await input.type(address); 52 | await expect(isRequiredErrorTriggered).toBeChecked(); 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /tests/ignore.spec.ts: -------------------------------------------------------------------------------- 1 | import test, { expect } from "@playwright/test"; 2 | 3 | test("suf-ignore", async ({ page }) => { 4 | await page.goto("examples/ignore-attribute"); 5 | const registeredList = page.locator('[id="registered"]'); 6 | await expect(registeredList).toHaveText("emailname"); // as emailname is the list content: - email - name 7 | }); 8 | -------------------------------------------------------------------------------- /tests/login.spec.ts: -------------------------------------------------------------------------------- 1 | import test, { expect } from "@playwright/test"; 2 | 3 | test("Login Example", async ({ page }) => { 4 | await page.goto("examples/login"); 5 | const email = page.getByPlaceholder("Email"); 6 | const password = page.getByPlaceholder("password"); 7 | const button = page.locator("button"); 8 | 9 | await expect(button).toBeDisabled(); 10 | await expect(email).not.toHaveClass("touched"); 11 | await expect(password).not.toHaveClass("touched"); 12 | 13 | await email.type("max@"); 14 | await expect(email).toHaveClass("touched"); 15 | await expect(password).not.toHaveClass("touched"); 16 | await password.type("random pass"); 17 | await expect(button).toBeDisabled(); 18 | 19 | await email.type("example.com"); 20 | await expect(password).toHaveClass("touched"); 21 | await expect(button).toBeEnabled(); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/multiforms.spec.ts: -------------------------------------------------------------------------------- 1 | import test, { expect } from "@playwright/test"; 2 | 3 | test("multi-forms", async ({ page }) => { 4 | await page.goto("examples/multi-forms"); 5 | 6 | // Forms 7 | const form1 = page.locator('[id="form-one"]'); 8 | const form2 = page.locator('[id="form-two"]'); 9 | 10 | // Form 1 11 | const form1Input = form1.locator('[id="input-form-one"]'); 12 | const form1Submit = form1.locator('[id="submit-form-one"]'); 13 | const form1HintMin = form1.locator('[id="hint-min-form-one"]'); 14 | const form1HintMax = form1.locator('[id="hint-max-form-one"]'); 15 | 16 | // Form 2 17 | const form2Input = form2.locator('[id="input-form-two"]'); 18 | const form2Submit = form2.locator('[id="submit-form-two"]'); 19 | const form2HintMin = form2.locator('[id="hint-min-form-two"]'); 20 | const form2HintMax = form2.locator('[id="hint-max-form-two"]'); 21 | 22 | // Form one tests: 23 | await expect(form1Submit).toBeDisabled(); 24 | await expect(form1HintMin).toBeHidden(); 25 | await expect(form1HintMax).toBeHidden(); 26 | await form1Input.fill("12"); 27 | await form1Input.blur(); 28 | await expect(form1HintMin).toBeVisible(); 29 | await expect(form1HintMax).toBeHidden(); 30 | // Form 2 hints should be hidden as well 31 | await expect(form2HintMin).toBeHidden(); 32 | await expect(form2HintMax).toBeHidden(); 33 | await form1Input.fill("123456"); 34 | await form1Input.blur(); 35 | await expect(form1HintMin).toBeHidden(); 36 | await expect(form1HintMax).toBeVisible(); 37 | // Form 2 hints should be hidden as well 38 | await expect(form2HintMin).toBeHidden(); 39 | await expect(form2HintMax).toBeHidden(); 40 | await form1Input.fill("1234"); 41 | await form1Input.blur(); 42 | await expect(form1HintMin).toBeHidden(); 43 | await expect(form1HintMax).toBeHidden(); 44 | await expect(form1Submit).toBeEnabled(); 45 | 46 | // Form two tests: 47 | await expect(form2Submit).toBeDisabled(); 48 | await expect(form2HintMin).toBeHidden(); 49 | await expect(form2HintMax).toBeHidden(); 50 | await form2Input.fill("12"); 51 | await form2Input.blur(); 52 | await expect(form2HintMin).toBeVisible(); 53 | await expect(form2HintMax).toBeHidden(); 54 | // Form 1 hints should be hidden as well 55 | await expect(form1HintMin).toBeHidden(); 56 | await expect(form1HintMax).toBeHidden(); 57 | await form2Input.fill("12345678"); 58 | await form2Input.blur(); 59 | await expect(form2HintMin).toBeHidden(); 60 | await expect(form2HintMax).toBeVisible(); 61 | // Form 1 hints should be hidden as well 62 | await expect(form1HintMin).toBeHidden(); 63 | await expect(form1HintMax).toBeHidden(); 64 | await form2Input.fill("123456"); 65 | await form2Input.blur(); 66 | await expect(form2HintMin).toBeHidden(); 67 | await expect(form2HintMax).toBeHidden(); 68 | await expect(form2Submit).toBeEnabled(); 69 | }); 70 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias 14 | // 15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 16 | // from the referenced tsconfig.json - TypeScript does not merge them in 17 | } 18 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { sveltekit } from "@sveltejs/kit/vite"; 2 | import type { UserConfig } from "vite"; 3 | 4 | const config: UserConfig = { 5 | plugins: [sveltekit()], 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@esbuild/android-arm@0.15.14": 6 | version "0.15.14" 7 | resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.15.14.tgz#5d0027f920eeeac313c01fd6ecb8af50c306a466" 8 | integrity sha512-+Rb20XXxRGisNu2WmNKk+scpanb7nL5yhuI1KR9wQFiC43ddPj/V1fmNyzlFC9bKiG4mYzxW7egtoHVcynr+OA== 9 | 10 | "@esbuild/linux-loong64@0.15.14": 11 | version "0.15.14" 12 | resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.15.14.tgz#1221684955c44385f8af34f7240088b7dc08d19d" 13 | integrity sha512-eQi9rosGNVQFJyJWV0HCA5WZae/qWIQME7s8/j8DMvnylfBv62Pbu+zJ2eUDqNf2O4u3WB+OEXyfkpBoe194sg== 14 | 15 | "@jridgewell/resolve-uri@3.1.0": 16 | version "3.1.0" 17 | resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" 18 | integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== 19 | 20 | "@jridgewell/sourcemap-codec@1.4.14": 21 | version "1.4.14" 22 | resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" 23 | integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== 24 | 25 | "@jridgewell/trace-mapping@^0.3.9": 26 | version "0.3.17" 27 | resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" 28 | integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== 29 | dependencies: 30 | "@jridgewell/resolve-uri" "3.1.0" 31 | "@jridgewell/sourcemap-codec" "1.4.14" 32 | 33 | "@nodelib/fs.scandir@2.1.5": 34 | version "2.1.5" 35 | resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" 36 | integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== 37 | dependencies: 38 | "@nodelib/fs.stat" "2.0.5" 39 | run-parallel "^1.1.9" 40 | 41 | "@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": 42 | version "2.0.5" 43 | resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" 44 | integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== 45 | 46 | "@nodelib/fs.walk@^1.2.3": 47 | version "1.2.8" 48 | resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" 49 | integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== 50 | dependencies: 51 | "@nodelib/fs.scandir" "2.1.5" 52 | fastq "^1.6.0" 53 | 54 | "@playwright/test@^1.36.1": 55 | version "1.36.1" 56 | resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.36.1.tgz#0b1247d279f142ac0876ce25e7daf15439d5367b" 57 | integrity sha512-YK7yGWK0N3C2QInPU6iaf/L3N95dlGdbsezLya4n0ZCh3IL7VgPGxC6Gnznh9ApWdOmkJeleT2kMTcWPRZvzqg== 58 | dependencies: 59 | "@types/node" "*" 60 | playwright-core "1.36.1" 61 | optionalDependencies: 62 | fsevents "2.3.2" 63 | 64 | "@polka/url@^1.0.0-next.20": 65 | version "1.0.0-next.21" 66 | resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" 67 | integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== 68 | 69 | "@sveltejs/adapter-auto@next": 70 | version "1.0.0-next.88" 71 | resolved "https://registry.yarnpkg.com/@sveltejs/adapter-auto/-/adapter-auto-1.0.0-next.88.tgz#21fd124443252bd3b9e164da7013cea1696034e1" 72 | integrity sha512-WcbELnu0Dz/72T1gbhDHxmNevVMEnQeNVi2IYrEj/uEKSMet4LOIrPv3DAAl8NETgBfzNJbMQpE7r9yL67CMTA== 73 | dependencies: 74 | import-meta-resolve "^2.1.0" 75 | 76 | "@sveltejs/kit@next": 77 | version "1.0.0-next.553" 78 | resolved "https://registry.yarnpkg.com/@sveltejs/kit/-/kit-1.0.0-next.553.tgz#5c3a4b013a80a8d8c542087245d7723ab8350101" 79 | integrity sha512-6gUPzmry+j/0uFJnGAJNru3lUpGY12We1Vsom5nDeRaG/Cs2wCwK8gMO2B5taZsHmryIB8zA+v0QCcu5kClmWA== 80 | dependencies: 81 | "@sveltejs/vite-plugin-svelte" "^1.1.0" 82 | "@types/cookie" "^0.5.1" 83 | cookie "^0.5.0" 84 | devalue "^4.2.0" 85 | kleur "^4.1.5" 86 | magic-string "^0.26.7" 87 | mime "^3.0.0" 88 | sade "^1.8.1" 89 | set-cookie-parser "^2.5.1" 90 | sirv "^2.0.2" 91 | tiny-glob "^0.2.9" 92 | undici "5.12.0" 93 | 94 | "@sveltejs/package@^1.0.0-next.6": 95 | version "1.0.0-next.6" 96 | resolved "https://registry.yarnpkg.com/@sveltejs/package/-/package-1.0.0-next.6.tgz#b6acf4e6b81eda66e9efd897d54a85e212533daf" 97 | integrity sha512-EwekVYRnD1r0deTt+9OKRvDopdX59FdfMVV+sN/LtIRO8+mEYuuDkj4uurt5yA9o2sMsv9xiAIWT5qKrq0rXuQ== 98 | dependencies: 99 | chokidar "^3.5.3" 100 | kleur "^4.1.5" 101 | sade "^1.8.1" 102 | svelte2tsx "~0.5.20" 103 | 104 | "@sveltejs/vite-plugin-svelte@^1.1.0": 105 | version "1.2.0" 106 | resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.2.0.tgz#35755db7ab4e2fa9b47f0c5ca8fd439d62f34546" 107 | integrity sha512-DT2oUkWAloH1tO7X5cQ4uDxQofaIS76skyFMElKtoqT6HJao+D82LI5i+0jPaSSmO7ex3Pa6jGYMlWy9ZJ1cdQ== 108 | dependencies: 109 | debug "^4.3.4" 110 | deepmerge "^4.2.2" 111 | kleur "^4.1.5" 112 | magic-string "^0.26.7" 113 | svelte-hmr "^0.15.1" 114 | vitefu "^0.2.1" 115 | 116 | "@types/cookie@^0.5.1": 117 | version "0.5.1" 118 | resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.5.1.tgz#b29aa1f91a59f35e29ff8f7cb24faf1a3a750554" 119 | integrity sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g== 120 | 121 | "@types/node@*": 122 | version "18.11.9" 123 | resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.9.tgz#02d013de7058cea16d36168ef2fc653464cfbad4" 124 | integrity sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg== 125 | 126 | "@types/pug@^2.0.4": 127 | version "2.0.6" 128 | resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6" 129 | integrity sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg== 130 | 131 | "@types/sass@^1.16.0": 132 | version "1.43.1" 133 | resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.43.1.tgz#86bb0168e9e881d7dade6eba16c9ed6d25dc2f68" 134 | integrity sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g== 135 | dependencies: 136 | "@types/node" "*" 137 | 138 | anymatch@~3.1.2: 139 | version "3.1.2" 140 | resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" 141 | integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== 142 | dependencies: 143 | normalize-path "^3.0.0" 144 | picomatch "^2.0.4" 145 | 146 | balanced-match@^1.0.0: 147 | version "1.0.2" 148 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" 149 | integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== 150 | 151 | binary-extensions@^2.0.0: 152 | version "2.2.0" 153 | resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" 154 | integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== 155 | 156 | brace-expansion@^1.1.7: 157 | version "1.1.11" 158 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 159 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 160 | dependencies: 161 | balanced-match "^1.0.0" 162 | concat-map "0.0.1" 163 | 164 | braces@^3.0.2, braces@~3.0.2: 165 | version "3.0.2" 166 | resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" 167 | integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== 168 | dependencies: 169 | fill-range "^7.0.1" 170 | 171 | buffer-crc32@^0.2.5: 172 | version "0.2.13" 173 | resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" 174 | integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== 175 | 176 | busboy@^1.6.0: 177 | version "1.6.0" 178 | resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" 179 | integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== 180 | dependencies: 181 | streamsearch "^1.1.0" 182 | 183 | callsites@^3.0.0: 184 | version "3.1.0" 185 | resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" 186 | integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== 187 | 188 | chokidar@^3.4.1, chokidar@^3.5.3: 189 | version "3.5.3" 190 | resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" 191 | integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== 192 | dependencies: 193 | anymatch "~3.1.2" 194 | braces "~3.0.2" 195 | glob-parent "~5.1.2" 196 | is-binary-path "~2.1.0" 197 | is-glob "~4.0.1" 198 | normalize-path "~3.0.0" 199 | readdirp "~3.6.0" 200 | optionalDependencies: 201 | fsevents "~2.3.2" 202 | 203 | concat-map@0.0.1: 204 | version "0.0.1" 205 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 206 | integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== 207 | 208 | cookie@^0.5.0: 209 | version "0.5.0" 210 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" 211 | integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== 212 | 213 | debug@^4.3.4: 214 | version "4.3.4" 215 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" 216 | integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== 217 | dependencies: 218 | ms "2.1.2" 219 | 220 | dedent-js@^1.0.1: 221 | version "1.0.1" 222 | resolved "https://registry.yarnpkg.com/dedent-js/-/dedent-js-1.0.1.tgz#bee5fb7c9e727d85dffa24590d10ec1ab1255305" 223 | integrity sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ== 224 | 225 | deepmerge@^4.2.2: 226 | version "4.2.2" 227 | resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955" 228 | integrity sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg== 229 | 230 | detect-indent@^6.0.0: 231 | version "6.1.0" 232 | resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" 233 | integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== 234 | 235 | devalue@^4.2.0: 236 | version "4.2.0" 237 | resolved "https://registry.yarnpkg.com/devalue/-/devalue-4.2.0.tgz#3f542d7c828e317bab5fd3bcecde210af8f83d4b" 238 | integrity sha512-mbjoAaCL2qogBKgeFxFPOXAUsZchircF+B/79LD4sHH0+NHfYm8gZpQrskKDn5gENGt35+5OI1GUF7hLVnkPDw== 239 | 240 | es6-promise@^3.1.2: 241 | version "3.3.1" 242 | resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" 243 | integrity sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg== 244 | 245 | esbuild-android-64@0.15.14: 246 | version "0.15.14" 247 | resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.15.14.tgz#114e55b0d58fb7b45d7fa3d93516bd13fc8869cc" 248 | integrity sha512-HuilVIb4rk9abT4U6bcFdU35UHOzcWVGLSjEmC58OVr96q5UiRqzDtWjPlCMugjhgUGKEs8Zf4ueIvYbOStbIg== 249 | 250 | esbuild-android-arm64@0.15.14: 251 | version "0.15.14" 252 | resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.15.14.tgz#8541f38a9aacf88e574fb13f5ad4ca51a04c12bb" 253 | integrity sha512-/QnxRVxsR2Vtf3XottAHj7hENAMW2wCs6S+OZcAbc/8nlhbAL/bCQRCVD78VtI5mdwqWkVi3wMqM94kScQCgqg== 254 | 255 | esbuild-darwin-64@0.15.14: 256 | version "0.15.14" 257 | resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.15.14.tgz#b40b334db81ff1e3677a6712b23761748a157c57" 258 | integrity sha512-ToNuf1uifu8hhwWvoZJGCdLIX/1zpo8cOGnT0XAhDQXiKOKYaotVNx7pOVB1f+wHoWwTLInrOmh3EmA7Fd+8Vg== 259 | 260 | esbuild-darwin-arm64@0.15.14: 261 | version "0.15.14" 262 | resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.14.tgz#44b5c1477bb7bdb852dd905e906f68765e2828bc" 263 | integrity sha512-KgGP+y77GszfYJgceO0Wi/PiRtYo5y2Xo9rhBUpxTPaBgWDJ14gqYN0+NMbu+qC2fykxXaipHxN4Scaj9tUS1A== 264 | 265 | esbuild-freebsd-64@0.15.14: 266 | version "0.15.14" 267 | resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.14.tgz#8c57315d238690f34b6ed0c94e5cfc04c858247a" 268 | integrity sha512-xr0E2n5lyWw3uFSwwUXHc0EcaBDtsal/iIfLioflHdhAe10KSctV978Te7YsfnsMKzcoGeS366+tqbCXdqDHQA== 269 | 270 | esbuild-freebsd-arm64@0.15.14: 271 | version "0.15.14" 272 | resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.14.tgz#2e92acca09258daa849e635565f52469266f0b7b" 273 | integrity sha512-8XH96sOQ4b1LhMlO10eEWOjEngmZ2oyw3pW4o8kvBcpF6pULr56eeYVP5radtgw54g3T8nKHDHYEI5AItvskZg== 274 | 275 | esbuild-linux-32@0.15.14: 276 | version "0.15.14" 277 | resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.15.14.tgz#ca5ed3e9dff82df486ddde362d7e00775a597dfd" 278 | integrity sha512-6ssnvwaTAi8AzKN8By2V0nS+WF5jTP7SfuK6sStGnDP7MCJo/4zHgM9oE1eQTS2jPmo3D673rckuCzRlig+HMA== 279 | 280 | esbuild-linux-64@0.15.14: 281 | version "0.15.14" 282 | resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.15.14.tgz#42952e1d08a299d5f573c567639fb37b033befbf" 283 | integrity sha512-ONySx3U0wAJOJuxGUlXBWxVKFVpWv88JEv0NZ6NlHknmDd1yCbf4AEdClSgLrqKQDXYywmw4gYDvdLsS6z0hcw== 284 | 285 | esbuild-linux-arm64@0.15.14: 286 | version "0.15.14" 287 | resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.14.tgz#0c0d788099703327ec0ae70758cb2639ef6c5d88" 288 | integrity sha512-kle2Ov6a1e5AjlHlMQl1e+c4myGTeggrRzArQFmWp6O6JoqqB9hT+B28EW4tjFWgV/NxUq46pWYpgaWXsXRPAg== 289 | 290 | esbuild-linux-arm@0.15.14: 291 | version "0.15.14" 292 | resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.15.14.tgz#751a5ca5042cd60f669b07c3bcec3dd6c4f8151c" 293 | integrity sha512-D2LImAIV3QzL7lHURyCHBkycVFbKwkDb1XEUWan+2fb4qfW7qAeUtul7ZIcIwFKZgPcl+6gKZmvLgPSj26RQ2Q== 294 | 295 | esbuild-linux-mips64le@0.15.14: 296 | version "0.15.14" 297 | resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.14.tgz#da8ac35f2704de0b52bf53a99c12f604fbe9b916" 298 | integrity sha512-FVdMYIzOLXUq+OE7XYKesuEAqZhmAIV6qOoYahvUp93oXy0MOVTP370ECbPfGXXUdlvc0TNgkJa3YhEwyZ6MRA== 299 | 300 | esbuild-linux-ppc64le@0.15.14: 301 | version "0.15.14" 302 | resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.14.tgz#a315b5016917429080c3d32e03319f1ff876ac55" 303 | integrity sha512-2NzH+iuzMDA+jjtPjuIz/OhRDf8tzbQ1tRZJI//aT25o1HKc0reMMXxKIYq/8nSHXiJSnYV4ODzTiv45s+h73w== 304 | 305 | esbuild-linux-riscv64@0.15.14: 306 | version "0.15.14" 307 | resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.14.tgz#9f2e0a935e5086d398fc19c7ff5d217bfefe3e12" 308 | integrity sha512-VqxvutZNlQxmUNS7Ac+aczttLEoHBJ9e3OYGqnULrfipRvG97qLrAv9EUY9iSrRKBqeEbSvS9bSfstZqwz0T4Q== 309 | 310 | esbuild-linux-s390x@0.15.14: 311 | version "0.15.14" 312 | resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.14.tgz#53108112faff5a4e1bad17f7b0b0ffa1df4b7efb" 313 | integrity sha512-+KVHEUshX5n6VP6Vp/AKv9fZIl5kr2ph8EUFmQUJnDpHwcfTSn2AQgYYm0HTBR2Mr4d0Wlr0FxF/Cs5pbFgiOw== 314 | 315 | esbuild-netbsd-64@0.15.14: 316 | version "0.15.14" 317 | resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.14.tgz#5330efc41fe4f1c2bab5462bcfe7a4ffce7ba00a" 318 | integrity sha512-6D/dr17piEgevIm1xJfZP2SjB9Z+g8ERhNnBdlZPBWZl+KSPUKLGF13AbvC+nzGh8IxOH2TyTIdRMvKMP0nEzQ== 319 | 320 | esbuild-openbsd-64@0.15.14: 321 | version "0.15.14" 322 | resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.14.tgz#ee64944d863e937611fc31adf349e9bb4f5f7eac" 323 | integrity sha512-rREQBIlMibBetgr2E9Lywt2Qxv2ZdpmYahR4IUlAQ1Efv/A5gYdO0/VIN3iowDbCNTLxp0bb57Vf0LFcffD6kA== 324 | 325 | esbuild-sunos-64@0.15.14: 326 | version "0.15.14" 327 | resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.15.14.tgz#29b0b20de6fe6ef50f9fbe533ec20dc4b595f9aa" 328 | integrity sha512-DNVjSp/BY4IfwtdUAvWGIDaIjJXY5KI4uD82+15v6k/w7px9dnaDaJJ2R6Mu+KCgr5oklmFc0KjBjh311Gxl9Q== 329 | 330 | esbuild-windows-32@0.15.14: 331 | version "0.15.14" 332 | resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.15.14.tgz#05e9b159d664809f7a4a8a68ed048d193457b27d" 333 | integrity sha512-pHBWrcA+/oLgvViuG9FO3kNPO635gkoVrRQwe6ZY1S0jdET07xe2toUvQoJQ8KT3/OkxqUasIty5hpuKFLD+eg== 334 | 335 | esbuild-windows-64@0.15.14: 336 | version "0.15.14" 337 | resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.15.14.tgz#d5ae086728ab30b72969e40ed0a7a0d9082f2cdd" 338 | integrity sha512-CszIGQVk/P8FOS5UgAH4hKc9zOaFo69fe+k1rqgBHx3CSK3Opyk5lwYriIamaWOVjBt7IwEP6NALz+tkVWdFog== 339 | 340 | esbuild-windows-arm64@0.15.14: 341 | version "0.15.14" 342 | resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.14.tgz#8eb50ab9a0ecaf058593fbad17502749306f801d" 343 | integrity sha512-KW9W4psdZceaS9A7Jsgl4WialOznSURvqX/oHZk3gOP7KbjtHLSsnmSvNdzagGJfxbAe30UVGXRe8q8nDsOSQw== 344 | 345 | esbuild@^0.15.9: 346 | version "0.15.14" 347 | resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.15.14.tgz#09202b811f1710363d5088a3401a351351c79875" 348 | integrity sha512-pJN8j42fvWLFWwSMG4luuupl2Me7mxciUOsMegKvwCmhEbJ2covUdFnihxm0FMIBV+cbwbtMoHgMCCI+pj1btQ== 349 | optionalDependencies: 350 | "@esbuild/android-arm" "0.15.14" 351 | "@esbuild/linux-loong64" "0.15.14" 352 | esbuild-android-64 "0.15.14" 353 | esbuild-android-arm64 "0.15.14" 354 | esbuild-darwin-64 "0.15.14" 355 | esbuild-darwin-arm64 "0.15.14" 356 | esbuild-freebsd-64 "0.15.14" 357 | esbuild-freebsd-arm64 "0.15.14" 358 | esbuild-linux-32 "0.15.14" 359 | esbuild-linux-64 "0.15.14" 360 | esbuild-linux-arm "0.15.14" 361 | esbuild-linux-arm64 "0.15.14" 362 | esbuild-linux-mips64le "0.15.14" 363 | esbuild-linux-ppc64le "0.15.14" 364 | esbuild-linux-riscv64 "0.15.14" 365 | esbuild-linux-s390x "0.15.14" 366 | esbuild-netbsd-64 "0.15.14" 367 | esbuild-openbsd-64 "0.15.14" 368 | esbuild-sunos-64 "0.15.14" 369 | esbuild-windows-32 "0.15.14" 370 | esbuild-windows-64 "0.15.14" 371 | esbuild-windows-arm64 "0.15.14" 372 | 373 | fast-glob@^3.2.7: 374 | version "3.2.12" 375 | resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" 376 | integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== 377 | dependencies: 378 | "@nodelib/fs.stat" "^2.0.2" 379 | "@nodelib/fs.walk" "^1.2.3" 380 | glob-parent "^5.1.2" 381 | merge2 "^1.3.0" 382 | micromatch "^4.0.4" 383 | 384 | fastq@^1.6.0: 385 | version "1.13.0" 386 | resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" 387 | integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== 388 | dependencies: 389 | reusify "^1.0.4" 390 | 391 | fill-range@^7.0.1: 392 | version "7.0.1" 393 | resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" 394 | integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== 395 | dependencies: 396 | to-regex-range "^5.0.1" 397 | 398 | fs.realpath@^1.0.0: 399 | version "1.0.0" 400 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 401 | integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== 402 | 403 | fsevents@2.3.2, fsevents@~2.3.2: 404 | version "2.3.2" 405 | resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" 406 | integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== 407 | 408 | function-bind@^1.1.1: 409 | version "1.1.1" 410 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" 411 | integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== 412 | 413 | glob-parent@^5.1.2, glob-parent@~5.1.2: 414 | version "5.1.2" 415 | resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" 416 | integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== 417 | dependencies: 418 | is-glob "^4.0.1" 419 | 420 | glob@^7.1.3: 421 | version "7.2.3" 422 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" 423 | integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== 424 | dependencies: 425 | fs.realpath "^1.0.0" 426 | inflight "^1.0.4" 427 | inherits "2" 428 | minimatch "^3.1.1" 429 | once "^1.3.0" 430 | path-is-absolute "^1.0.0" 431 | 432 | globalyzer@0.1.0: 433 | version "0.1.0" 434 | resolved "https://registry.yarnpkg.com/globalyzer/-/globalyzer-0.1.0.tgz#cb76da79555669a1519d5a8edf093afaa0bf1465" 435 | integrity sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q== 436 | 437 | globrex@^0.1.2: 438 | version "0.1.2" 439 | resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" 440 | integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== 441 | 442 | graceful-fs@^4.1.3: 443 | version "4.2.10" 444 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" 445 | integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== 446 | 447 | has@^1.0.3: 448 | version "1.0.3" 449 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" 450 | integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== 451 | dependencies: 452 | function-bind "^1.1.1" 453 | 454 | import-fresh@^3.2.1: 455 | version "3.3.0" 456 | resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" 457 | integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== 458 | dependencies: 459 | parent-module "^1.0.0" 460 | resolve-from "^4.0.0" 461 | 462 | import-meta-resolve@^2.1.0: 463 | version "2.1.0" 464 | resolved "https://registry.yarnpkg.com/import-meta-resolve/-/import-meta-resolve-2.1.0.tgz#c8952d331ed6e9bb6ad524a7549deb3d34af41ce" 465 | integrity sha512-yG9pxkWJVTy4cmRsNWE3ztFdtFuYIV8G4N+cbCkO8b+qngkLyIUhxQFuZ0qJm67+0nUOxjMPT7nfksPKza1v2g== 466 | 467 | inflight@^1.0.4: 468 | version "1.0.6" 469 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 470 | integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== 471 | dependencies: 472 | once "^1.3.0" 473 | wrappy "1" 474 | 475 | inherits@2: 476 | version "2.0.4" 477 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" 478 | integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== 479 | 480 | is-binary-path@~2.1.0: 481 | version "2.1.0" 482 | resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" 483 | integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== 484 | dependencies: 485 | binary-extensions "^2.0.0" 486 | 487 | is-core-module@^2.9.0: 488 | version "2.11.0" 489 | resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" 490 | integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== 491 | dependencies: 492 | has "^1.0.3" 493 | 494 | is-extglob@^2.1.1: 495 | version "2.1.1" 496 | resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" 497 | integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== 498 | 499 | is-glob@^4.0.1, is-glob@~4.0.1: 500 | version "4.0.3" 501 | resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" 502 | integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== 503 | dependencies: 504 | is-extglob "^2.1.1" 505 | 506 | is-number@^7.0.0: 507 | version "7.0.0" 508 | resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" 509 | integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== 510 | 511 | kleur@^4.1.5: 512 | version "4.1.5" 513 | resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" 514 | integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== 515 | 516 | lower-case@^2.0.2: 517 | version "2.0.2" 518 | resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" 519 | integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== 520 | dependencies: 521 | tslib "^2.0.3" 522 | 523 | magic-string@^0.25.7: 524 | version "0.25.9" 525 | resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" 526 | integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== 527 | dependencies: 528 | sourcemap-codec "^1.4.8" 529 | 530 | magic-string@^0.26.7: 531 | version "0.26.7" 532 | resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.7.tgz#caf7daf61b34e9982f8228c4527474dac8981d6f" 533 | integrity sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow== 534 | dependencies: 535 | sourcemap-codec "^1.4.8" 536 | 537 | merge2@^1.3.0: 538 | version "1.4.1" 539 | resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" 540 | integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== 541 | 542 | micromatch@^4.0.4: 543 | version "4.0.5" 544 | resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" 545 | integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== 546 | dependencies: 547 | braces "^3.0.2" 548 | picomatch "^2.3.1" 549 | 550 | mime@^3.0.0: 551 | version "3.0.0" 552 | resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" 553 | integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== 554 | 555 | min-indent@^1.0.0: 556 | version "1.0.1" 557 | resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" 558 | integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== 559 | 560 | minimatch@^3.1.1: 561 | version "3.1.2" 562 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" 563 | integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== 564 | dependencies: 565 | brace-expansion "^1.1.7" 566 | 567 | minimist@^1.2.0, minimist@^1.2.6: 568 | version "1.2.7" 569 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" 570 | integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== 571 | 572 | mkdirp@^0.5.1: 573 | version "0.5.6" 574 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6" 575 | integrity sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw== 576 | dependencies: 577 | minimist "^1.2.6" 578 | 579 | mri@^1.1.0: 580 | version "1.2.0" 581 | resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" 582 | integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== 583 | 584 | mrmime@^1.0.0: 585 | version "1.0.1" 586 | resolved "https://registry.yarnpkg.com/mrmime/-/mrmime-1.0.1.tgz#5f90c825fad4bdd41dc914eff5d1a8cfdaf24f27" 587 | integrity sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw== 588 | 589 | ms@2.1.2: 590 | version "2.1.2" 591 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" 592 | integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== 593 | 594 | nanoid@^3.3.4: 595 | version "3.3.4" 596 | resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" 597 | integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== 598 | 599 | no-case@^3.0.4: 600 | version "3.0.4" 601 | resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" 602 | integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== 603 | dependencies: 604 | lower-case "^2.0.2" 605 | tslib "^2.0.3" 606 | 607 | normalize-path@^3.0.0, normalize-path@~3.0.0: 608 | version "3.0.0" 609 | resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" 610 | integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== 611 | 612 | once@^1.3.0: 613 | version "1.4.0" 614 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 615 | integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== 616 | dependencies: 617 | wrappy "1" 618 | 619 | parent-module@^1.0.0: 620 | version "1.0.1" 621 | resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" 622 | integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== 623 | dependencies: 624 | callsites "^3.0.0" 625 | 626 | pascal-case@^3.1.1: 627 | version "3.1.2" 628 | resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" 629 | integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== 630 | dependencies: 631 | no-case "^3.0.4" 632 | tslib "^2.0.3" 633 | 634 | path-is-absolute@^1.0.0: 635 | version "1.0.1" 636 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 637 | integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== 638 | 639 | path-parse@^1.0.7: 640 | version "1.0.7" 641 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" 642 | integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== 643 | 644 | picocolors@^1.0.0: 645 | version "1.0.0" 646 | resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" 647 | integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== 648 | 649 | picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: 650 | version "2.3.1" 651 | resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" 652 | integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== 653 | 654 | playwright-core@1.36.1: 655 | version "1.36.1" 656 | resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.36.1.tgz#f5f275d70548768ca892583519c89b237a381c77" 657 | integrity sha512-7+tmPuMcEW4xeCL9cp9KxmYpQYHKkyjwoXRnoeTowaeNat8PoBMk/HwCYhqkH2fRkshfKEOiVus/IhID2Pg8kg== 658 | 659 | postcss@^8.4.18: 660 | version "8.4.19" 661 | resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.19.tgz#61178e2add236b17351897c8bcc0b4c8ecab56fc" 662 | integrity sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA== 663 | dependencies: 664 | nanoid "^3.3.4" 665 | picocolors "^1.0.0" 666 | source-map-js "^1.0.2" 667 | 668 | prettier-plugin-svelte@^2.8.1: 669 | version "2.8.1" 670 | resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-2.8.1.tgz#5b7df5bd0d953b133281607295a3e5131052bb8b" 671 | integrity sha512-KA3K1J3/wKDnCxW7ZDRA/QL2Q67N7Xs3gOERqJ5X1qFjq1DdnN3K1R29scSKwh+kA8FF67pXbYytUpvN/i3iQw== 672 | 673 | prettier@^2.7.1: 674 | version "2.7.1" 675 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" 676 | integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== 677 | 678 | queue-microtask@^1.2.2: 679 | version "1.2.3" 680 | resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" 681 | integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== 682 | 683 | readdirp@~3.6.0: 684 | version "3.6.0" 685 | resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" 686 | integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== 687 | dependencies: 688 | picomatch "^2.2.1" 689 | 690 | resolve-from@^4.0.0: 691 | version "4.0.0" 692 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" 693 | integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== 694 | 695 | resolve@^1.22.1: 696 | version "1.22.1" 697 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" 698 | integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== 699 | dependencies: 700 | is-core-module "^2.9.0" 701 | path-parse "^1.0.7" 702 | supports-preserve-symlinks-flag "^1.0.0" 703 | 704 | reusify@^1.0.4: 705 | version "1.0.4" 706 | resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" 707 | integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== 708 | 709 | rimraf@^2.5.2: 710 | version "2.7.1" 711 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" 712 | integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== 713 | dependencies: 714 | glob "^7.1.3" 715 | 716 | rollup@^2.79.1: 717 | version "2.79.1" 718 | resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.1.tgz#bedee8faef7c9f93a2647ac0108748f497f081c7" 719 | integrity sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw== 720 | optionalDependencies: 721 | fsevents "~2.3.2" 722 | 723 | run-parallel@^1.1.9: 724 | version "1.2.0" 725 | resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" 726 | integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== 727 | dependencies: 728 | queue-microtask "^1.2.2" 729 | 730 | sade@^1.7.4, sade@^1.8.1: 731 | version "1.8.1" 732 | resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" 733 | integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== 734 | dependencies: 735 | mri "^1.1.0" 736 | 737 | sander@^0.5.0: 738 | version "0.5.1" 739 | resolved "https://registry.yarnpkg.com/sander/-/sander-0.5.1.tgz#741e245e231f07cafb6fdf0f133adfa216a502ad" 740 | integrity sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA== 741 | dependencies: 742 | es6-promise "^3.1.2" 743 | graceful-fs "^4.1.3" 744 | mkdirp "^0.5.1" 745 | rimraf "^2.5.2" 746 | 747 | set-cookie-parser@^2.5.1: 748 | version "2.5.1" 749 | resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.5.1.tgz#ddd3e9a566b0e8e0862aca974a6ac0e01349430b" 750 | integrity sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ== 751 | 752 | sirv@^2.0.2: 753 | version "2.0.2" 754 | resolved "https://registry.yarnpkg.com/sirv/-/sirv-2.0.2.tgz#128b9a628d77568139cff85703ad5497c46a4760" 755 | integrity sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w== 756 | dependencies: 757 | "@polka/url" "^1.0.0-next.20" 758 | mrmime "^1.0.0" 759 | totalist "^3.0.0" 760 | 761 | sorcery@^0.10.0: 762 | version "0.10.0" 763 | resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.10.0.tgz#8ae90ad7d7cb05fc59f1ab0c637845d5c15a52b7" 764 | integrity sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g== 765 | dependencies: 766 | buffer-crc32 "^0.2.5" 767 | minimist "^1.2.0" 768 | sander "^0.5.0" 769 | sourcemap-codec "^1.3.0" 770 | 771 | source-map-js@^1.0.2: 772 | version "1.0.2" 773 | resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" 774 | integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== 775 | 776 | sourcemap-codec@^1.3.0, sourcemap-codec@^1.4.8: 777 | version "1.4.8" 778 | resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" 779 | integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== 780 | 781 | streamsearch@^1.1.0: 782 | version "1.1.0" 783 | resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" 784 | integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== 785 | 786 | strip-indent@^3.0.0: 787 | version "3.0.0" 788 | resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" 789 | integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== 790 | dependencies: 791 | min-indent "^1.0.0" 792 | 793 | supports-preserve-symlinks-flag@^1.0.0: 794 | version "1.0.0" 795 | resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" 796 | integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== 797 | 798 | svelte-check@^2.7.1: 799 | version "2.9.2" 800 | resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-2.9.2.tgz#6581b750aa51cf476e82139a674a9bfa4de2ec2d" 801 | integrity sha512-DRi8HhnCiqiGR2YF9ervPGvtoYrheE09cXieCTEqeTPOTJzfoa54Py8rovIBv4bH4n5HgZYIyTQ3DDLHQLl2uQ== 802 | dependencies: 803 | "@jridgewell/trace-mapping" "^0.3.9" 804 | chokidar "^3.4.1" 805 | fast-glob "^3.2.7" 806 | import-fresh "^3.2.1" 807 | picocolors "^1.0.0" 808 | sade "^1.7.4" 809 | svelte-preprocess "^4.0.0" 810 | typescript "*" 811 | 812 | svelte-hmr@^0.15.1: 813 | version "0.15.1" 814 | resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.15.1.tgz#d11d878a0bbb12ec1cba030f580cd2049f4ec86b" 815 | integrity sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA== 816 | 817 | svelte-preprocess@^4.0.0, svelte-preprocess@^4.10.6: 818 | version "4.10.7" 819 | resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-4.10.7.tgz#3626de472f51ffe20c9bc71eff5a3da66797c362" 820 | integrity sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw== 821 | dependencies: 822 | "@types/pug" "^2.0.4" 823 | "@types/sass" "^1.16.0" 824 | detect-indent "^6.0.0" 825 | magic-string "^0.25.7" 826 | sorcery "^0.10.0" 827 | strip-indent "^3.0.0" 828 | 829 | svelte2tsx@~0.5.20: 830 | version "0.5.20" 831 | resolved "https://registry.yarnpkg.com/svelte2tsx/-/svelte2tsx-0.5.20.tgz#0ac5411b53d6b6913b12e12a02c03ce9eb13e945" 832 | integrity sha512-yNHmN/uoAnJ7d1XqVohiNA6TMFOxibHyEddUAHVt1PiLXtbwAJF3WaGYlg8QbOdoXzOVsVNCAlqRUIdULUm+OA== 833 | dependencies: 834 | dedent-js "^1.0.1" 835 | pascal-case "^3.1.1" 836 | 837 | svelte@^3.46.0: 838 | version "3.59.2" 839 | resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.59.2.tgz#a137b28e025a181292b2ae2e3dca90bf8ec73aec" 840 | integrity sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA== 841 | 842 | tiny-glob@^0.2.9: 843 | version "0.2.9" 844 | resolved "https://registry.yarnpkg.com/tiny-glob/-/tiny-glob-0.2.9.tgz#2212d441ac17928033b110f8b3640683129d31e2" 845 | integrity sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg== 846 | dependencies: 847 | globalyzer "0.1.0" 848 | globrex "^0.1.2" 849 | 850 | to-regex-range@^5.0.1: 851 | version "5.0.1" 852 | resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" 853 | integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== 854 | dependencies: 855 | is-number "^7.0.0" 856 | 857 | totalist@^3.0.0: 858 | version "3.0.0" 859 | resolved "https://registry.yarnpkg.com/totalist/-/totalist-3.0.0.tgz#4ef9c58c5f095255cdc3ff2a0a55091c57a3a1bd" 860 | integrity sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw== 861 | 862 | tslib@^2.0.3, tslib@^2.3.1: 863 | version "2.4.1" 864 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" 865 | integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== 866 | 867 | typescript@*, typescript@^4.7.4: 868 | version "4.9.3" 869 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.3.tgz#3aea307c1746b8c384435d8ac36b8a2e580d85db" 870 | integrity sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA== 871 | 872 | undici@5.12.0: 873 | version "5.12.0" 874 | resolved "https://registry.yarnpkg.com/undici/-/undici-5.12.0.tgz#c758ffa704fbcd40d506e4948860ccaf4099f531" 875 | integrity sha512-zMLamCG62PGjd9HHMpo05bSLvvwWOZgGeiWlN/vlqu3+lRo3elxktVGEyLMX+IO7c2eflLjcW74AlkhEZm15mg== 876 | dependencies: 877 | busboy "^1.6.0" 878 | 879 | vite@^3.1.0: 880 | version "3.2.4" 881 | resolved "https://registry.yarnpkg.com/vite/-/vite-3.2.4.tgz#d8c7892dd4268064e04fffbe7d866207dd24166e" 882 | integrity sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw== 883 | dependencies: 884 | esbuild "^0.15.9" 885 | postcss "^8.4.18" 886 | resolve "^1.22.1" 887 | rollup "^2.79.1" 888 | optionalDependencies: 889 | fsevents "~2.3.2" 890 | 891 | vitefu@^0.2.1: 892 | version "0.2.1" 893 | resolved "https://registry.yarnpkg.com/vitefu/-/vitefu-0.2.1.tgz#9dcd78737c77b366206706dac2403a751903907d" 894 | integrity sha512-clkvXTAeUf+XQKm3bhWUhT4pye+3acm6YCTGaWhxxIvZZ/QjnA3JA8Zud+z/mO5y5XYvJJhevs5Sjkv/FI8nRw== 895 | 896 | wrappy@1: 897 | version "1.0.2" 898 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 899 | integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== 900 | --------------------------------------------------------------------------------