form-father 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,26 +1,35 @@
1
1
  {
2
2
  "name": "form-father",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Form Father: Библиотека для обработки форм",
5
5
  "type": "module",
6
- "files": ["dist", "README.md", "LICENSE", "RESPONSE_API.md", "CHANGELOG.md", "docs", "demos"],
6
+ "main": "index.js",
7
+ "module": "index.js",
8
+ "types": "index.d.ts",
7
9
  "exports": {
8
10
  ".": {
9
- "import": "./dist/index.js",
10
- "types": "./dist/index.d.ts"
11
- }
11
+ "types": "./index.d.ts",
12
+ "import": "./index.js",
13
+ "default": "./index.js"
14
+ },
15
+ "./FormFather.min.js": "./FormFather.min.js"
12
16
  },
17
+ "unpkg": "./FormFather.min.js",
18
+ "jsdelivr": "./FormFather.min.js",
19
+ "files": [
20
+ "index.js",
21
+ "index.d.ts",
22
+ "FormFather.min.js",
23
+ "*.map",
24
+ "types",
25
+ "README.md",
26
+ "LICENSE",
27
+ "RESPONSE_API.md"
28
+ ],
13
29
  "scripts": {
14
30
  "build": "rollup -c",
15
31
  "watch": "rollup -c -w",
16
32
  "release": "node scripts/release.js",
17
- "release:check": "npm test -- --runInBand --watchman=false && npm run docs:check && npm run build && npm run smoke:package && npm run pack:dry-run",
18
- "docs:check": "node scripts/check-docs.mjs",
19
- "test": "jest --coverage",
20
- "prepublishOnly": "npm test -- --runInBand --watchman=false && npm run build",
21
- "pack:dry-run": "npm_config_cache=/tmp/form-father-npm-cache npm pack --dry-run",
22
- "smoke:package": "node scripts/smoke-package.mjs",
23
- "versions:sync": "node scripts/sync-version.js",
24
33
  "demos": "BUILD_TARGET=demos npm run build && concurrently --kill-others \"vite\" \"npm run watch\""
25
34
  },
26
35
  "repository": {
@@ -48,15 +57,10 @@
48
57
  "@rollup/plugin-json": "^6.1.0",
49
58
  "@rollup/plugin-node-resolve": "^15.3.0",
50
59
  "@rollup/plugin-terser": "^0.4.4",
51
- "@types/jest": "^30.0.0",
52
- "@types/jsdom": "^21.1.7",
53
60
  "chalk": "^5.3.0",
54
61
  "concurrently": "^9.0.1",
55
62
  "fs-extra": "^11.2.0",
56
63
  "inquirer": "^12.1.0",
57
- "jest": "^30.0.5",
58
- "jest-environment-jsdom": "^30.0.5",
59
- "jsdom": "^26.1.0",
60
64
  "ora": "^8.1.1",
61
65
  "postcss-preset-env": "^10.0.8",
62
66
  "prettier-plugin-jsdoc": "^1.3.0",
@@ -66,9 +70,7 @@
66
70
  "rollup-plugin-dts": "^6.1.1",
67
71
  "rollup-plugin-typescript2": "^0.36.0",
68
72
  "sass-embedded": "^1.80.5",
69
- "ts-jest": "^29.4.0",
70
- "ts-node": "^10.9.2",
71
73
  "typescript": "^5.6.3",
72
74
  "vite": "^5.4.10"
73
75
  }
74
- }
76
+ }
package/CHANGELOG.md DELETED
@@ -1,154 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project are documented here.
4
-
5
- ## 0.7.0 - 2026-04-27
6
-
7
- ### Added
8
-
9
- - Added `docs/api/README.md` with a compact public API, options, methods, validators, adapters, and helpers reference.
10
- - Added `docs/demo/README.md` with manual demo scenarios and demo coverage notes.
11
- - Added a Public API demo panel covering `setValues()`, `validateField()`, `setErrors()`, `getValues()`, and
12
- `clearErrors()`.
13
- - Added `npm run docs:check` to verify documentation files, version references, package README sync, and demo coverage.
14
-
15
- ### Changed
16
-
17
- - `npm run release:check` now includes documentation checks before build and package smoke checks.
18
- - Package smoke checks now verify the broader public export surface and package documentation files.
19
-
20
- ### Tests
21
-
22
- - Release gate remains at 122 tests plus docs/package smoke checks.
23
-
24
- ## 0.6.0 - 2026-04-27
25
-
26
- ### Added
27
-
28
- - Added accessible field error wiring via `aria-describedby` with safe preservation of existing descriptions.
29
- - Added `errorSummary` option with default rendering, custom selector support, focus control, and custom renderers.
30
- - Added `ariaDescribeErrors` and `errorIdPrefix` options for UI/a11y integration control.
31
-
32
- ### Changed
33
-
34
- - Field, form-level, backend, and manual errors can now share one visible summary while keeping the existing inline error
35
- rendering path.
36
- - Demo forms now include accessible error summaries.
37
-
38
- ### Tests
39
-
40
- - Test suite expanded to 122 tests.
41
- - Coverage baseline: 98.30% statements, 86.88% branches, 97.25% functions, 100% lines.
42
-
43
- ## 0.5.0 - 2026-04-27
44
-
45
- ### Added
46
-
47
- - Added field validator DX helpers: `registerFieldValidator()`, `createPatternValidator()`, and
48
- `createLengthValidator()`.
49
- - Added form validator DX helpers: `createFormValidator()`, `sameAsField()`, `requiredIf()`, and `dateOrder()`.
50
- - Added helper types for reusable form validator predicates and issue factories.
51
-
52
- ### Changed
53
-
54
- - Demo and documentation now use higher-level helper APIs for common password-confirm and validation rule recipes.
55
-
56
- ### Tests
57
-
58
- - Test suite expanded to 115 tests.
59
- - Coverage baseline: 98.19% statements, 86.91% branches, 97.04% functions, 100% lines.
60
-
61
- ## 0.4.0 - 2026-04-27
62
-
63
- ### Added
64
-
65
- - Added form-level and cross-field validation via `formValidators`.
66
- - Added `setErrors()` for backend maps, `ErrorResponse[]`, global form errors, and custom validation issue objects.
67
- - Added `FORM_ERROR_FIELD` constant for form-level errors in `getErrors()`.
68
- - Added public TypeScript types for form validation contexts and issues.
69
-
70
- ### Changed
71
-
72
- - `validate()` now runs field-level rules first, then form-level validators over `getValues()`.
73
- - Cross-field and backend errors share the same rendering path as field validation errors.
74
-
75
- ### Tests
76
-
77
- - Test suite expanded to 113 tests.
78
- - Coverage baseline: 98.31% statements, 87.05% branches, 96.34% functions, 100% lines.
79
-
80
- ## 0.3.0 - 2026-04-25
81
-
82
- ### Added
83
-
84
- - Added convenience API: `Form.initAll()`, `updateOptions()`, public `submit()`, `validateField()`, `showFieldError()`,
85
- `getErrors()`, `getValues()`, `setValues()`, `clearErrors()`, and `reset()`.
86
- - Added client validation hooks: `onValidationError`, `onBeforeSubmit`, and `onSubmitError`.
87
- - Added live validation options: `validateOn`, `revalidateOn`, `validationDebounce`, and `focusFirstErroredInput`.
88
- - Added `data-validate`, `data-error-*`, and `data-error-container` support while keeping `data-custom-validate`.
89
- - Added optional mutation observing for dynamic fields and submit buttons.
90
- - Added race-safe async field validation state with `aria-busy` and configurable field state attributes.
91
- - Added dependency-free schema adapters: `createSchemaValidator()`, `registerSchemaValidator()`, and
92
- `createFieldValidator()`.
93
- - Added static demos for login, callback/server errors, GET search, and multipart upload.
94
- - Added recipes documentation for common API, data-attribute, adapter, and server-error flows.
95
-
96
- ### Changed
97
-
98
- - Validation now merges every matching schema rule for a field before applying data-attribute rules.
99
- - Server-side field errors now populate the public `getErrors()` list.
100
- - Package contents now include docs, demos, and the changelog.
101
-
102
- ### Tests
103
-
104
- - Test suite expanded to 109 tests.
105
- - Coverage baseline: 98.10% statements, 86.15% branches, 95.69% functions, 100% lines.
106
-
107
- ## 0.2.11 - 2026-04-25
108
-
109
- ### Added
110
-
111
- - Added release checklist, package smoke checks, CI workflow, and release gate scripts.
112
- - Added npm pack dry-run script with a temporary npm cache for local cache permission issues.
113
-
114
- ### Changed
115
-
116
- - Updated package metadata and release workflow after `0.2.10` had already been published.
117
-
118
- ## 0.2.10 - 2026-04-25
119
-
120
- ### Added
121
-
122
- - Exported public helper APIs from the package entrypoint: `serializeToFormData`, validators, URL/email/phone helpers, scroll helpers, and response helpers.
123
- - Exported TypeScript types for public consumers: `FormOptions`, `ValidationRule`, `ValidationSchema`, `ErrorResponse`, and `ResponseBody`.
124
- - Added package smoke checks for built files and public exports.
125
- - Added CI workflow for tests, build, smoke checks, and npm pack dry-run.
126
- - Added coverage thresholds to keep the stabilization baseline from regressing.
127
-
128
- ### Changed
129
-
130
- - `new Form(formEl)` now works without an options object.
131
- - Submit detection now supports implicit submit buttons (`<button>`) and `input[type="image"]`.
132
- - `GET` and `HEAD` forms now append form data to the action query string and do not send a request body.
133
- - `wrapData` now applies consistently across supported encodings: `application/x-www-form-urlencoded`, `multipart/form-data`, `text/plain`, and `application/json`.
134
- - Non-200 HTTP responses, `success !== true`, and invalid JSON responses now flow through `onResponseUnsuccess`.
135
- - Server-side field errors can be applied to `input`, `textarea`, and `select`.
136
- - `serializeToFormData` now skips disabled controls, controls marked with `data-no-serialize`, and button controls.
137
- - Built-in validators are registered idempotently across repeated module imports.
138
-
139
- ### Fixed
140
-
141
- - Loader and `waitResponse` cleanup now runs when `fetch` or response parsing fails.
142
- - Required radio groups now produce one validation error per group.
143
- - Field error rendering no longer requires a wrapper-level `showError()` method.
144
- - Release script now exits with `0` on success and handles missing temporary files during cleanup.
145
- - Rollup IIFE build no longer warns about mixed default and named exports.
146
-
147
- ### Tests
148
-
149
- - Test coverage baseline: 98.32% statements, 86.34% branches, 93.18% functions, 100% lines.
150
- - Test suite baseline: 93 tests passing.
151
-
152
- ## 0.2.9
153
-
154
- - Previous published patch release.
package/RESPONSE_API.md DELETED
@@ -1,233 +0,0 @@
1
- ## Краткое введение
2
-
3
- Эй, ребята! Собрались здесь для того, чтобы понять, как у нас отвечает сервер после того, как отправляем формы через
4
- AJAX. Это будет не какая-то скучная докуха, а всё по-кубански, с размахом и только по сути!
5
-
6
- ## Что и как 🛠️
7
-
8
- ### `success`
9
-
10
- - **Тип**: boolean.
11
- - **Описание**: Флаг, показывающий, всё ли круто.
12
- - **Как Работает**: Если сервер справился, то `true`, а если нет – `false`.
13
-
14
- ### `redirect-url`
15
-
16
- - **Тип**: string (опционально).
17
- - **Описание**: Куда пойти, если всё зашибись.
18
- - **Когда Появляется**: Только если `success` равно `true`.
19
-
20
- ### `redirect-url-delay`
21
-
22
- - **Тип**: number (опционально).
23
- - **Описание**: Задержка в ms перед редиректом.
24
-
25
- ### `reload`
26
-
27
- - **Тип**: boolean (опционально).
28
- - **Описание**: Как перезагрузить страничку.
29
- - **Когда Используем**: Когда сервер скажет "обновись".
30
-
31
- ### `reload-delay`
32
-
33
- - **Тип**: number (опционально).
34
- - **Описание**: Задержка в ms перед перезагрузкой страницы.
35
-
36
- ### `error`
37
-
38
- - **Тип**: boolean (опционально).
39
- - **Описание**: Флаг о проблемах.
40
- - **Как Работает**: Если есть косяк, то `true`.
41
-
42
- ### `error-msg`
43
-
44
- - **Тип**: string (опционально).
45
- - **Описание**: Пояснения от сервера о косяке.
46
- - **Пример**: "Тут беда, братишка".
47
-
48
- ### `errors`
49
-
50
- - **Тип**: array (опционально).
51
- - **Описание**: Список мелких недоразумений.
52
- - **Содержание**: Конкретные поля и тексты ошибок.
53
-
54
- Каждый объект в массиве содержит:
55
-
56
- - `name` (string): Имя поля, в котором произошла ошибка.
57
- - `error-msg` (string): Текст ошибки для данного поля.
58
-
59
- ### `error-toast`
60
-
61
- - Тип: string (опционально).
62
- - Описание: Выводится всплывашка(toast) с ошибкой.
63
- - Пример: "Тут беда, братишка".
64
-
65
- > [!Caution]
66
- > `error-toast` DEPRECATED!!!
67
-
68
- ### `toast`
69
- - Тип: объект | массив объектов.
70
- - Описание: Выводит определённый тип тоста и передаёт опции.
71
- - Пример, один тост:
72
- ```
73
- {
74
- type: 'success',
75
- title: 'Успешно',
76
- }
77
- ```
78
-
79
- - Пример, несколько тостов за раз:
80
- ```
81
- [
82
- {
83
- type: 'success',
84
- title: 'Успешно',
85
- },
86
- {
87
- type: 'error',
88
- title: 'Ошибка',
89
- }
90
- ]
91
- ```
92
-
93
-
94
- ## Примеры ответов 📝
95
-
96
- ### `Когда всё чик-пук, и можно идти дальше.`
97
-
98
- ```json
99
- {
100
- "success": true,
101
- }
102
- ```
103
-
104
- ### `Если нужен редирект.`
105
-
106
- ```json
107
- {
108
- "success": true,
109
- "redirect-url": "https://example.com/success",
110
- "redirect-url-delay": 5000, // если нужна задержка перед редиректом
111
- }
112
- ```
113
-
114
- ### `Если нужна перезагрузка страницы.`
115
-
116
- ```json
117
- {
118
- "success": true,
119
- "reload": true,
120
- "reload-delay": 5000, // если нужна задержка перед обновлением страницы
121
- }
122
- ```
123
-
124
- ### `Когда надо что-то менять.`
125
-
126
- ```json
127
- {
128
- "success": false,
129
- "error": true,
130
- "error-msg": "Тут беда, братишка",
131
- "errors": [
132
- {
133
- "name": "email",
134
- "error-msg": "Эй, мэйл какой-то кривой"
135
- }
136
- ]
137
- }
138
- ```
139
-
140
- Вот и всё, ребятки! Теперь вы знаете, как наш сервер отзывается на ваши AJAX-формы. Дерзайте!
141
-
142
- # Как работать с e-commerce
143
-
144
- - В сниппете #src/snippets/\_ym-metrika.html лежит пример кода инициализации Яндекс.Метрики.
145
- - В скрипте с id='ecommerceScript' есть 2 важных момента: первый - это пример запуска необходимого кода сразу после
146
- инициализации Яндекс.метрики, а второй - это функция, которая принимает данные и с учётом этого происходят действия
147
- необходимые для метрик.
148
- - На данный момент существует только 4 события: `click`, `submit`, `ajax-success` и `ajax-fail`.
149
- - Атрибут `data-ecom-data` нужно указывать, только для тех событий, которые не ожидают ответа от сервера.
150
-
151
- ## Примеры использования атрибутов:
152
-
153
- ### Обычный клик на ссылке
154
-
155
- ```html
156
- <a
157
- href="#"
158
- data-ecom-action="click"
159
- data-ecom-data='{
160
- "click": {
161
- "ymCounter123123123": "pull-right",
162
- "dataLayer": []
163
- }
164
- }'
165
- >
166
- Оформить заказ
167
- </a>
168
- ```
169
-
170
- ### Множественное количество событий на Форме-кнопке
171
-
172
- ```html
173
- <button
174
- data-form-button
175
- data-form-button-action="/endpoint"
176
- data-form-button-method="POST"
177
- data-form-button-data='{"someProperties": "1234"}'
178
- data-ecom-action="click,ajax-success,submit"
179
- data-ecom-data='{
180
- "click": {
181
- "ymCounter123123123": "pull-right",
182
- "dataLayer": []
183
- },
184
- "submit": {
185
- "ymCounter123123123": "pull-right",
186
- "dataLayer": []
187
- }
188
- }'
189
- >
190
- Оформить заказ
191
- </button>
192
- ```
193
-
194
- ### Cобытие у элемента формы
195
-
196
- ```html
197
- <form
198
- class="ordering-page__body"
199
- action="/api/order/"
200
- method="POST"
201
- name="ORDER_FORM"
202
- enctype="multipart/form-data"
203
- novalidate
204
- data-ecom-action="ajax-success"
205
- >
206
- ...
207
- </form>
208
- ```
209
-
210
- # Сериализация форм и полей
211
-
212
- ## Работа с атрибутом `data-no-serialize-before-changes`
213
-
214
- ### Общее описание
215
-
216
- Атрибут `data-no-serialize-before-changes` предназначен для контроля сериализации полей формы в AJAX-запросах. С его
217
- помощью можно указать, какие поля формы не должны быть сериализованы и отправлены на сервер до того момента, как в них
218
- произойдут изменения со стороны пользователя.
219
-
220
- ### Применение
221
-
222
- Для использования атрибута достаточно добавить его к любому элементу формы, например, `<input>`, `<select>` или
223
- `<textarea>`. Пока пользователь не изменит значение такого поля, оно не будет включено в сериализованные данные формы
224
- при AJAX-запросе.
225
-
226
- ### Пример
227
-
228
- ```html
229
- <input type="text" name="user-email" data-no-serialize-before-changes />
230
- ```
231
-
232
- В данном случае, поле для ввода email пользователя не будет сериализовано и отправлено на сервер до тех пор, пока
233
- пользователь явно не изменит его содержимое.
package/demos/index.html DELETED
@@ -1,198 +0,0 @@
1
- <!doctype html>
2
- <html lang="ru">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Form Father demos</title>
7
- <link rel="stylesheet" href="./styles.css" />
8
- </head>
9
- <body>
10
- <header class="demo-header">
11
- <div>
12
- <p class="eyebrow">Form Father</p>
13
- <h1>Демо форм, валидации и ответов сервера</h1>
14
- </div>
15
- <a class="github-link" href="https://github.com/Poliklot/form-father">GitHub</a>
16
- </header>
17
-
18
- <main class="demo-grid">
19
- <section class="demo-panel">
20
- <div class="panel-title">
21
- <p class="eyebrow">Login</p>
22
- <h2>Live validation</h2>
23
- </div>
24
- <form data-form-father action="/demo/login" method="post" enctype="application/json" data-demo="login" novalidate>
25
- <div data-form-father-summary hidden></div>
26
-
27
- <label class="field">
28
- <span>Email</span>
29
- <input
30
- class="input"
31
- name="email"
32
- type="email"
33
- autocomplete="email"
34
- data-validate="required|email|available-email"
35
- data-error-required="Email обязателен"
36
- data-error-email="Введите корректный email"
37
- data-error-available-email="Этот email уже занят"
38
- data-error-container="#login-email-error"
39
- />
40
- <small id="login-email-error" class="field-error" hidden></small>
41
- </label>
42
-
43
- <label class="field">
44
- <span>Password</span>
45
- <input
46
- class="input"
47
- name="password"
48
- type="password"
49
- autocomplete="current-password"
50
- data-validate="required|min-length:6"
51
- data-error-required="Пароль обязателен"
52
- data-error-min-length="Минимум 6 символов"
53
- />
54
- </label>
55
-
56
- <label class="field">
57
- <span>Repeat password</span>
58
- <input
59
- class="input"
60
- name="passwordConfirm"
61
- type="password"
62
- autocomplete="current-password"
63
- data-validate="required"
64
- data-error-required="Повторите пароль"
65
- />
66
- </label>
67
-
68
- <button type="submit">Войти</button>
69
- </form>
70
- </section>
71
-
72
- <section class="demo-panel">
73
- <div class="panel-title">
74
- <p class="eyebrow">Callback</p>
75
- <h2>Server field errors</h2>
76
- </div>
77
- <form data-form-father action="/demo/callback" method="post" data-demo="callback" novalidate>
78
- <div data-form-father-summary hidden></div>
79
-
80
- <label class="field">
81
- <span>Имя</span>
82
- <input class="input" name="name" data-validate="required" data-error-required="Укажите имя" />
83
- </label>
84
-
85
- <label class="field">
86
- <span>Телефон</span>
87
- <input
88
- class="input"
89
- name="tel"
90
- type="tel"
91
- placeholder="+79991234567"
92
- data-validate="required|tel"
93
- data-error-tel="Телефон в формате +7XXXXXXXXXX"
94
- />
95
- </label>
96
-
97
- <button type="submit">Заказать звонок</button>
98
- </form>
99
- </section>
100
-
101
- <section class="demo-panel">
102
- <div class="panel-title">
103
- <p class="eyebrow">Search</p>
104
- <h2>GET query</h2>
105
- </div>
106
- <form data-form-father action="/demo/search?source=demos" method="get" data-demo="search" novalidate>
107
- <div data-form-father-summary hidden></div>
108
-
109
- <label class="field">
110
- <span>Запрос</span>
111
- <input class="input" name="q" value="form father" data-validate="required" />
112
- </label>
113
-
114
- <label class="field">
115
- <span>Раздел</span>
116
- <select class="input" name="section">
117
- <option value="docs">Docs</option>
118
- <option value="api">API</option>
119
- <option value="examples">Examples</option>
120
- </select>
121
- </label>
122
-
123
- <button type="submit">Найти</button>
124
- </form>
125
- </section>
126
-
127
- <section class="demo-panel">
128
- <div class="panel-title">
129
- <p class="eyebrow">Multipart</p>
130
- <h2>FormData upload</h2>
131
- </div>
132
- <form data-form-father action="/demo/upload" method="post" enctype="multipart/form-data" data-demo="upload" novalidate>
133
- <div data-form-father-summary hidden></div>
134
-
135
- <label class="field">
136
- <span>Название</span>
137
- <input class="input" name="title" data-validate="required" />
138
- </label>
139
-
140
- <label class="field">
141
- <span>Файл</span>
142
- <input class="input" name="attachment" type="file" />
143
- </label>
144
-
145
- <button type="submit">Отправить</button>
146
- </form>
147
- </section>
148
-
149
- <section class="demo-panel demo-panel--wide">
150
- <div class="panel-title">
151
- <p class="eyebrow">Public API</p>
152
- <h2>Programmatic control</h2>
153
- </div>
154
- <form data-form-father action="/demo/api" method="post" data-demo="api" novalidate>
155
- <div data-form-father-summary hidden></div>
156
-
157
- <label class="field">
158
- <span>Имя</span>
159
- <input class="input" name="name" data-validate="required" data-error-required="Укажите имя" />
160
- </label>
161
-
162
- <label class="field">
163
- <span>Email</span>
164
- <input class="input" name="email" type="email" data-validate="required|email" />
165
- </label>
166
-
167
- <label class="field">
168
- <span>Тариф</span>
169
- <select class="input" name="plan">
170
- <option value="starter">Starter</option>
171
- <option value="team">Team</option>
172
- <option value="enterprise">Enterprise</option>
173
- </select>
174
- </label>
175
-
176
- <div class="demo-actions" aria-label="Public API actions">
177
- <button type="button" data-api-action="fill">setValues()</button>
178
- <button type="button" data-api-action="validate-email">validateField()</button>
179
- <button type="button" data-api-action="server-errors">setErrors()</button>
180
- <button type="button" data-api-action="values">getValues()</button>
181
- <button type="button" data-api-action="clear">clearErrors()</button>
182
- </div>
183
- </form>
184
- </section>
185
- </main>
186
-
187
- <section class="demo-output">
188
- <div class="panel-title">
189
- <p class="eyebrow">Output</p>
190
- <h2>Последний ответ</h2>
191
- </div>
192
- <pre id="demo-output">Заполните любую форму.</pre>
193
- </section>
194
-
195
- <script src="../dist/FormFather.min.js"></script>
196
- <script src="./main.js"></script>
197
- </body>
198
- </html>