diff --git a/web/.gitignore b/web/.gitignore
new file mode 100644
index 0000000..40b878d
--- /dev/null
+++ b/web/.gitignore
@@ -0,0 +1 @@
+node_modules/
\ No newline at end of file
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..eee68d0
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,260 @@
+
+
+
+
+ GarminHomeAssistant
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/jsconfig.json b/web/jsconfig.json
new file mode 100644
index 0000000..aa5d751
--- /dev/null
+++ b/web/jsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "lib": ["esnext", "dom"],
+ "types": [
+ "./node_modules/monaco-editor/monaco.d.ts",
+ "./node_modules/json-ast-comments/lib/index.d.ts",
+ "./node_modules/@types/toastify-js/index.d.ts",
+ "./types.d.ts"
+ ]
+ },
+ "include": ["."]
+}
diff --git a/web/main.js b/web/main.js
new file mode 100644
index 0000000..9db4fd9
--- /dev/null
+++ b/web/main.js
@@ -0,0 +1,953 @@
+/**
+ * Get all entities in HomeAssistant.
+ * @param {string} api_url
+ * @param {string} api_token
+ * @returns {Promise>} [id, name]
+ */
+async function get_entities(api_url, api_token) {
+ const res = await fetch(api_url + '/template', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${api_token}`,
+ },
+ mode: 'cors',
+ body: `{"template":"[{% for entity in states %}[\\"{{ entity.entity_id }}\\",\\"{{ entity.name }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`,
+ });
+ if (res.status == 401 || res.status == 403) {
+ document.querySelector('#api_token').classList.add('invalid');
+ }
+ return Object.fromEntries(await res.json());
+}
+
+/**
+ * Get all devices in HomeAssistant.
+ * @param {string} api_url
+ * @param {string} api_token
+ * @returns {Promise>} [id, name]
+ */
+async function get_devices(api_url, api_token) {
+ const res = await fetch(api_url + '/template', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${api_token}`,
+ },
+ mode: 'cors',
+ body: `{"template":"{% set devices = states | map(attribute='entity_id') | map('device_id') | unique | reject('eq', None) | list %}[{% for device in devices %}[\\"{{ device }}\\",\\"{{ device_attr(device, 'name') }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`,
+ });
+ if (res.status == 401 || res.status == 403) {
+ document.querySelector('#api_token').classList.add('invalid');
+ }
+ return Object.fromEntries(await res.json());
+}
+
+/**
+ * Get all areas in HomeAssistant.
+ * @param {string} api_url
+ * @param {string} api_token
+ * @returns {Promise>} [id, name]
+ */
+async function get_areas(api_url, api_token) {
+ const res = await fetch(api_url + '/template', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${api_token}`,
+ },
+ mode: 'cors',
+ body: `{"template":"[{% for area in areas() %}[\\"{{ area }}\\",\\"{{ area_name(area) }}\\"]{% if not loop.last %},{% endif %}{% endfor %}]"}`,
+ });
+ if (res.status == 401 || res.status == 403) {
+ document.querySelector('#api_token').classList.add('invalid');
+ }
+ return Object.fromEntries(await res.json());
+}
+
+/**
+ * Get all services in HomeAssistant.
+ * @param {string} api_url
+ * @param {string} api_token
+ * @returns {Promise<[string, { name: string; description: string; fields: Record }][]>} [id, data]
+ */
+async function get_services(api_url, api_token) {
+ const res = await fetch(api_url + '/services', {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${api_token}`,
+ },
+ mode: 'cors',
+ });
+ if (res.status == 401 || res.status == 403) {
+ document.querySelector('#api_token').classList.add('invalid');
+ }
+ const data = await res.json();
+ const services = [];
+ for (const d of data) {
+ for (const service in d.services) {
+ services.push([`${d.domain}.${service}`, d.services[service]]);
+ }
+ }
+ return services;
+}
+
+/**
+ * Get schema from GitHub.
+ * @returns {Promise<{}>}
+ */
+async function get_schema() {
+ const res = await fetch(
+ 'https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json'
+ );
+ return res.json();
+}
+
+/**
+ * Generate schema for HomeAssistant.
+ * @param {Record} entities
+ * @param {Record} devices
+ * @param {Record} areas
+ * @param {[string, { name: string; description: string; fields: Record }][]} services
+ * @param {{}} schema
+ * @returns {Promise<{}>}
+ */
+async function generate_schema(entities, devices, areas, services, schema) {
+ schema.$defs.entity = {
+ enum: Object.keys(entities),
+ };
+ schema.$defs.device = {
+ enum: Object.keys(devices),
+ };
+ schema.$defs.area = {
+ enum: Object.keys(areas),
+ };
+
+ const oneOf = [];
+ for (const [id, data] of services) {
+ const i_properties = {
+ service: {
+ title: data.name,
+ description: data.description,
+ const: id,
+ },
+ data: {
+ type: 'object',
+ properties: {},
+ additionalProperties: false,
+ },
+ };
+ const required = [];
+ for (const [field, f] of Object.entries(data.fields)) {
+ i_properties.data.properties[field] = {
+ title: f.name,
+ description: f.description,
+ example: f.example,
+ };
+ if (f.required) {
+ required.push(field);
+ }
+
+ const selector = f.selector;
+ if (selector) {
+ if (Object.hasOwn(selector, 'action')) {
+ i_properties.data.properties[field].type = 'array';
+ i_properties.data.properties[field].items = {
+ $ref: '#/$defs/tap_action',
+ };
+ } else if (Object.hasOwn(selector, 'area')) {
+ if (selector.area?.multiple) {
+ i_properties.data.properties[field].type = 'array';
+ i_properties.data.properties[field].items = {
+ $ref: '#/$defs/area',
+ };
+ } else {
+ i_properties.data.properties[field].$ref = '#/$defs/area';
+ }
+ } else if (Object.hasOwn(selector, 'boolean')) {
+ i_properties.data.properties[field].type = 'boolean';
+ } else if (Object.hasOwn(selector, 'number')) {
+ i_properties.data.properties[field].type = 'number';
+ i_properties.data.properties[field].minimum = selector.number?.min;
+ i_properties.data.properties[field].maximum = selector.number?.max;
+ i_properties.data.properties[field].multipleOf =
+ selector.number?.step;
+ } else if (Object.hasOwn(selector, 'color_temp')) {
+ i_properties.data.properties[field].type = 'number';
+ i_properties.data.properties[field].minimum =
+ selector.color_temp?.min;
+ i_properties.data.properties[field].maximum =
+ selector.color_temp?.max;
+ i_properties.data.properties[field].multipleOf =
+ selector.color_temp?.step;
+ } else if (Object.hasOwn(selector, 'date')) {
+ i_properties.data.properties[field].type = 'string';
+ i_properties.data.properties[field].pattern =
+ '^\\d{4}-\\d{2}-\\d{2}$';
+ } else if (Object.hasOwn(selector, 'datetime')) {
+ i_properties.data.properties[field].type = 'string';
+ i_properties.data.properties[field].pattern =
+ '^\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}(\\:\\d{2})?$';
+ } else if (Object.hasOwn(selector, 'time')) {
+ i_properties.data.properties[field].type = 'string';
+ i_properties.data.properties[field].pattern =
+ '^\\d{2}:\\d{2}(\\:\\d{2})?$';
+ } else if (Object.hasOwn(selector, 'device')) {
+ if (selector.device?.multiple) {
+ i_properties.data.properties[field].type = 'array';
+ i_properties.data.properties[field].items = {
+ $ref: '#/$defs/device',
+ };
+ } else {
+ i_properties.data.properties[field].$ref = '#/$defs/device';
+ }
+ } else if (Object.hasOwn(selector, 'entity')) {
+ if (selector.entity?.multiple) {
+ i_properties.data.properties[field].type = 'array';
+ i_properties.data.properties[field].items = {
+ $ref: '#/$defs/entity',
+ };
+ } else {
+ i_properties.data.properties[field].$ref = '#/$defs/entity';
+ }
+ } else if (Object.hasOwn(selector, 'icon')) {
+ i_properties.data.properties[field].type = 'string';
+ i_properties.data.properties[field].pattern = '^[^.]+:[^.]+$';
+ } else if (Object.hasOwn(selector, 'location')) {
+ i_properties.data.properties[field].type = 'object';
+ i_properties.data.properties[field].properties = {
+ longitude: {
+ type: 'number',
+ },
+ latitude: {
+ type: 'number',
+ },
+ radius: {
+ type: 'number',
+ minimum: 0,
+ },
+ };
+ } else if (Object.hasOwn(selector, 'color_rgb')) {
+ i_properties.data.properties[field].type = 'array';
+ i_properties.data.properties[field].prefixItems = [
+ {
+ type: 'number',
+ minimum: 0,
+ maximum: 255,
+ multipleOf: 1,
+ },
+ {
+ type: 'number',
+ minimum: 0,
+ maximum: 255,
+ multipleOf: 1,
+ },
+ {
+ type: 'number',
+ minimum: 0,
+ maximum: 255,
+ multipleOf: 1,
+ },
+ ];
+ } else if (Object.hasOwn(selector, 'select')) {
+ const oneOf2 = [];
+ if (selector.select?.options) {
+ for (let o of selector.select.options) {
+ if (typeof o == 'string') {
+ oneOf2.push({
+ const: o,
+ });
+ } else {
+ oneOf2.push({
+ const: o.value || '',
+ });
+ }
+ }
+ }
+ if (selector.select?.custom) {
+ oneOf2.push({
+ type: 'string',
+ });
+ }
+ if (selector.select?.multiple) {
+ i_properties.data.properties[field].type = 'array';
+ i_properties.data.properties[field].items = {
+ oneOf: oneOf2,
+ };
+ } else {
+ i_properties.data.properties[field].oneOf = oneOf2;
+ }
+ } else if (Object.hasOwn(selector, 'state' in selector || 'template')) {
+ i_properties.data.properties[field].type = 'string';
+ } else if (Object.hasOwn(selector, 'text')) {
+ let pattern;
+ const p = selector.text?.type;
+ if (p == 'color') {
+ pattern = '^#[0-9a-fA-F]{6}$';
+ } else if (p == 'date') {
+ pattern = '^\\d{4}-\\d{2}-\\d{2}$';
+ } else if (p == 'datetime-local') {
+ pattern = '^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}$';
+ } else if (p == 'email') {
+ pattern =
+ '^([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x22([^\\x0d\\x22\\x5c\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x22)(\\x2e([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x22([^\\x0d\\x22\\x5c\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x22))*\\x40([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x5b([^\\x0d\\x5b-\\x5d\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x5d)(\\x2e([^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+|\\x5b([^\\x0d\\x5b-\\x5d\\x80-\\xff]|\\x5c[\\x00-\\x7f])*\\x5d))*$';
+ } else if (p == 'month') {
+ pattern = '^\\d{4}-\\d{2}$';
+ } else if (p == 'number') {
+ pattern = '^d*.?d+$';
+ } else if (p == 'time') {
+ pattern = '^\\d{2}:\\d{2}$';
+ } else if (p == 'url') {
+ pattern =
+ "^[a-z](?:[-a-z0-9\\+\\.])*:(?:\\/\\/(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD])*@)?(?:\\[(?:(?:(?:[0-9a-f]{1,4}:){6}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|::(?:[0-9a-f]{1,4}:){5}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){4}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:[0-9a-f]{1,4}:[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){3}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:(?:[0-9a-f]{1,4}:){0,2}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:){2}(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:(?:[0-9a-f]{1,4}:){0,3}[0-9a-f]{1,4})?::[0-9a-f]{1,4}:(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:(?:[0-9a-f]{1,4}:){0,4}[0-9a-f]{1,4})?::(?:[0-9a-f]{1,4}:[0-9a-f]{1,4}|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3})|(?:(?:[0-9a-f]{1,4}:){0,5}[0-9a-f]{1,4})?::[0-9a-f]{1,4}|(?:(?:[0-9a-f]{1,4}:){0,6}[0-9a-f]{1,4})?::)|v[0-9a-f]+[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:]+)\\]|(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])(?:\\.(?:[0-9]|[1-9][0-9]|1[0-9][0-9]|2[0-4][0-9]|25[0-5])){3}|(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=@\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD])*)(?::[0-9]*)?(?:\\/(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD]))*)*|\\/(?:(?:(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD]))+)(?:\\/(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD]))*)*)?|(?:(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD]))+)(?:\\/(?:(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD]))*)*|(?!(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD])))(?:\\?(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\/\\?\\xA0-\\uD7FF\\uE000-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E\\uDB80-\\uDBBE\\uDBC0-\\uDBFE][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F\\uDBBF\\uDBFF][\\uDC00-\\uDFFD])*)?(?:\\#(?:%[0-9a-f][0-9a-f]|[-a-z0-9\\._~!\\$&'\\(\\)\\*\\+,;=:@\\/\\?\\xA0-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFEF]|[\\uD800-\\uD83E\\uD840-\\uD87E\\uD880-\\uD8BE\\uD8C0-\\uD8FE\\uD900-\\uD93E\\uD940-\\uD97E\\uD980-\\uD9BE\\uD9C0-\\uD9FE\\uDA00-\\uDA3E\\uDA40-\\uDA7E\\uDA80-\\uDABE\\uDAC0-\\uDAFE\\uDB00-\\uDB3E\\uDB44-\\uDB7E][\\uDC00-\\uDFFF]|[\\uD83F\\uD87F\\uD8BF\\uD8FF\\uD93F\\uD97F\\uD9BF\\uD9FF\\uDA3F\\uDA7F\\uDABF\\uDAFF\\uDB3F\\uDB7F][\\uDC00-\\uDFFD])*)?$";
+ } else if (p == 'week') {
+ pattern = '^\\d{4}-W\\d{2}$';
+ }
+ if (selector.text?.multiple) {
+ i_properties.data.properties[field].type = 'array';
+ i_properties.data.properties[field].items = {
+ type: 'string',
+ pattern: pattern,
+ };
+ } else {
+ i_properties.data.properties[field].type = 'string';
+ i_properties.data.properties[field].pattern = pattern;
+ }
+ }
+ }
+ if (required.length > 0) {
+ i_properties.data.required = required;
+ }
+ }
+ oneOf.push({
+ title: data.name,
+ description: data.description,
+ properties: i_properties,
+ });
+ }
+ schema.$defs.tap_action = {
+ type: 'object',
+ oneOf: oneOf,
+ properties: {
+ service: {
+ type: 'string',
+ },
+ confirm: {
+ $ref: '#/$defs/confirm',
+ },
+ data: {
+ type: 'object',
+ properties: {},
+ },
+ },
+ };
+ delete schema.$defs.tap.properties.service;
+ delete schema.$schema;
+
+ return schema;
+}
+
+let api_url = localStorage.getItem('api_url') ?? '';
+let menu_url = localStorage.getItem('menu_url') ?? '';
+let api_token = localStorage.getItem('api_token') ?? '';
+document.querySelector('#api_url').value = api_url;
+document.querySelector('#menu_url').value = menu_url;
+document.querySelector('#api_token').value = api_token;
+
+document.querySelector('#troubleshooting').addEventListener('click', (e) => {
+ document.querySelector('#troubleshooting-dialog').showModal();
+});
+
+document.querySelector('#test-api').addEventListener('click', async (e) => {
+ try {
+ document.querySelector('#test-api-response').innerText = 'Testing...';
+ const res = await fetch(api_url + '/', {
+ headers: {
+ Authorization: `Bearer ${api_token}`,
+ },
+ cache: 'no-cache',
+ mode: 'cors',
+ });
+ const text = await res.text();
+ if (res.status == 200) {
+ document.querySelector('#test-api-response').innerText =
+ JSON.parse(text).message;
+ } else if (res.status == 400) {
+ try {
+ document.querySelector('#test-api-response').innerText =
+ JSON.parse(text).message;
+ } catch {
+ document.querySelector('#test-api-response').innerText = text;
+ }
+ } else if (res.status == 401 || res.status == 403) {
+ document.querySelector('#api_token').classList.add('invalid');
+ document.querySelector('#test-api-response').innerText = 'Invalid token.';
+ } else {
+ document.querySelector('#test-api-response').innerText = text;
+ }
+ } catch (e) {
+ document.querySelector('#test-api-response').innerText = e.message;
+ }
+});
+document.querySelector('#test-menu').addEventListener('click', async (e) => {
+ try {
+ document.querySelector('#test-menu-response').innerText = 'Testing...';
+ const res = await fetch(menu_url, {
+ cache: 'no-cache',
+ mode: 'cors',
+ });
+ if (res.status == 200) {
+ document.querySelector('#test-menu-response').innerText = 'Available';
+ } else if (res.status == 400) {
+ document.querySelector('#test-menu-response').innerText =
+ await res.text();
+ } else {
+ document.querySelector('#test-menu-response').innerText =
+ await res.text();
+ }
+ } catch (e) {
+ document.querySelector('#test-menu-response').innerText = e.message;
+ }
+});
+
+function get(d, p) {
+ for (let i = 0; i < p.length; i++) {
+ d = d[p[i]];
+ }
+ return d;
+}
+
+/**
+ * @param {{ text: string; color: string }} options
+ */
+function toast({ text, color }) {
+ const t = Toastify({
+ text,
+ gravity: 'bottom', // `top` or `bottom`
+ position: 'right', // `left`, `center` or `right`
+ stopOnFocus: true, // Prevents dismissing of toast on hover
+ // close: true,
+ style: {
+ background: 'var(--ctp-mocha-base)',
+ outline: '1px solid ' + (color ?? 'var(--ctp-mocha-blue)'),
+ },
+ });
+ t.showToast();
+ return t;
+}
+
+// require is provided by loader.min.js.
+require.config({
+ paths: {
+ vs: 'https://unpkg.com/monaco-editor@0.45.0/min/vs',
+ },
+});
+require(['vs/editor/editor.main'], async () => {
+ let entities, devices, areas, services, schema;
+ async function loadSchema() {
+ [entities, devices, areas, services, schema] = await Promise.all([
+ get_entities(api_url, api_token),
+ get_devices(api_url, api_token),
+ get_areas(api_url, api_token),
+ get_services(api_url, api_token),
+ get_schema(),
+ ]);
+ makeMarkers();
+ try {
+ schema = await generate_schema(
+ entities,
+ devices,
+ areas,
+ services,
+ schema
+ );
+ } catch {}
+ console.log(schema);
+ // configure the JSON language support with schemas and schema associations
+ monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
+ validate: true,
+ schemas: [
+ {
+ uri: 'https://raw.githubusercontent.com/house-of-abbey/GarminHomeAssistant/main/config.schema.json',
+ fileMatch: [modelUri.toString()],
+ schema,
+ },
+ ],
+ });
+ }
+
+ document.querySelector('#api_url').addEventListener('change', (e) => {
+ api_url = e.target.value;
+ localStorage.setItem('api_url', api_url);
+ document.querySelector('#test-api-response').innerText = 'Check now!';
+ loadSchema();
+ });
+ document.querySelector('#menu_url').addEventListener('change', (e) => {
+ menu_url = e.target.value;
+ localStorage.setItem('menu_url', menu_url);
+ document.querySelector('#test-menu-response').innerText = 'Check now!';
+ checkRemoteMenu();
+ });
+ document.querySelector('#api_token').addEventListener('change', (e) => {
+ api_token = e.target.value;
+ localStorage.setItem('api_token', api_token);
+ document.querySelector('#api_token').classList.remove('invalid');
+ document.querySelector('#test-api-response').innerText = 'Check now!';
+ loadSchema();
+ });
+ loadSchema();
+ checkRemoteMenu();
+
+ async function checkRemoteMenu() {
+ if (menu_url != '') {
+ const remote = await fetch(menu_url, {
+ cache: 'no-cache',
+ mode: 'cors',
+ });
+ if (remote.status == 200) {
+ document.querySelector('#menu_url').classList.remove('invalid');
+ const text = await remote.text();
+ if (model.getValue() === text) {
+ document.querySelector('#menu_url').classList.remove('outofsync');
+ } else {
+ document.querySelector('#menu_url').classList.add('outofsync');
+ }
+ } else {
+ document.querySelector('#menu_url').classList.add('invalid');
+ }
+ }
+ }
+
+ // Configures two JSON schemas, with references.
+
+ var modelUri = monaco.Uri.parse('/config/www/garmin/menu.json'); // a made up unique URI for our model
+ var model = monaco.editor.createModel(
+ localStorage.getItem('json') ?? '{}',
+ 'json',
+ modelUri
+ );
+
+ monaco.editor.defineTheme(
+ 'mocha',
+ await fetch(
+ 'https://josephabbey.github.io/catppuccin-monaco/mocha.json'
+ ).then((r) => r.json())
+ );
+
+ const editor = monaco.editor.create(document.getElementById('container'), {
+ model: model,
+ theme: 'mocha',
+ automaticLayout: true,
+ glyphMargin: true,
+ });
+
+ window.addEventListener('keydown', (e) => {
+ if (e.key == 's' && e.ctrlKey) {
+ e.preventDefault();
+ model.setValue(
+ JSON.stringify(JSON.parse(editor.getValue()), undefined, 2) + '\n'
+ );
+ }
+ });
+
+ var decorations = editor.createDecorationsCollection([]);
+
+ let markers = [];
+
+ const renderTemplate = editor.addCommand(
+ 0,
+ async function (_, template) {
+ const t = toast({
+ text: 'Rendering template...',
+ });
+ const res = await fetch(api_url + '/template', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${api_token}`,
+ },
+ mode: 'cors',
+ body: `{"template":"${template}"}`,
+ });
+ t.hideToast();
+ if (res.status == 200) {
+ toast({
+ text: await res.text(),
+ color: 'var(--ctp-mocha-green)',
+ });
+ } else if (res.status == 400) {
+ toast({
+ text: (await res.json()).message,
+ color: 'var(--ctp-mocha-red)',
+ });
+ } else if (res.status == 401 || res.status == 403) {
+ document.querySelector('#api_token').classList.add('invalid');
+ } else {
+ toast({
+ text: await res.text(),
+ color: 'var(--ctp-mocha-red)',
+ });
+ }
+ },
+ ''
+ );
+
+ const runAction = editor.addCommand(
+ 0,
+ async function (_, action) {
+ const service = action.tap_action.service.split('.');
+ let data = action.tap_action.data;
+ if (data) {
+ data.entity_id = action.entity;
+ } else {
+ data = {
+ entity_id: action.entity,
+ };
+ }
+ const t = toast({
+ text: 'Running action...',
+ });
+ const res = await fetch(
+ api_url + '/services/' + service[0] + '/' + service[1],
+ {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${api_token}`,
+ },
+ mode: 'cors',
+ body: JSON.stringify(data),
+ }
+ );
+ t.hideToast();
+ if (res.status == 200) {
+ toast({
+ text: 'Success',
+ color: 'var(--ctp-mocha-green)',
+ });
+ } else if (res.status == 400) {
+ const text = await res.text();
+ try {
+ toast({
+ text: JSON.parse(text).message,
+ color: 'var(--ctp-mocha-red)',
+ });
+ } catch {
+ toast({
+ text: text,
+ color: 'var(--ctp-mocha-red)',
+ });
+ }
+ } else if (res.status == 401 || res.status == 403) {
+ document.querySelector('#api_token').classList.add('invalid');
+ } else {
+ toast({
+ text: await res.text(),
+ color: 'var(--ctp-mocha-red)',
+ });
+ }
+ makeMarkers();
+ },
+ ''
+ );
+
+ const toggle = editor.addCommand(
+ 0,
+ async function (_, item) {
+ const entity = item.entity.split('.');
+ const t = toast({
+ text: 'Toggling...',
+ });
+ const res = await fetch(api_url + '/services/' + entity[0] + '/toggle', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${api_token}`,
+ },
+ mode: 'cors',
+ body: JSON.stringify({
+ entity_id: item.entity,
+ }),
+ });
+ t.hideToast();
+ if (res.status == 200) {
+ toast({
+ text: 'Success',
+ color: 'var(--ctp-mocha-green)',
+ });
+ } else if (res.status == 400) {
+ const text = await res.text();
+ try {
+ toast({
+ text: JSON.parse(text).message,
+ color: 'var(--ctp-mocha-red)',
+ });
+ } catch {
+ toast({
+ text: text,
+ color: 'var(--ctp-mocha-red)',
+ });
+ }
+ } else if (res.status == 401 || res.status == 403) {
+ document.querySelector('#api_token').classList.add('invalid');
+ } else {
+ toast({
+ text: await res.text(),
+ color: 'var(--ctp-mocha-red)',
+ });
+ }
+ makeMarkers();
+ },
+ ''
+ );
+
+ async function makeMarkers() {
+ try {
+ const ast = json.parse(model.getValue());
+ const data = JSON.parse(model.getValue());
+ markers = [];
+ const glyphs = [];
+ async function testToggle(range, entity) {
+ const res = await fetch(api_url + '/states/' + entity, {
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${api_token}`,
+ },
+ mode: 'cors',
+ });
+ const d = await res.json();
+ if (d.state == 'on') {
+ decorations.append([
+ {
+ range,
+ options: {
+ isWholeLine: true,
+ glyphMarginClassName: 'toggle_on',
+ },
+ },
+ ]);
+ } else {
+ decorations.append([
+ {
+ range,
+ options: {
+ isWholeLine: true,
+ glyphMarginClassName: 'toggle_off',
+ },
+ },
+ ]);
+ }
+ }
+ const toggles = [];
+ async function testTemplate(range, template) {
+ const l = model.getValueInRange(range);
+ let trim = 0;
+ while (trim < l.length && l[trim] == ' ') {
+ trim++;
+ }
+ trim++;
+ const res = await fetch(api_url + '/template', {
+ method: 'POST',
+ headers: {
+ Authorization: `Bearer ${api_token}`,
+ },
+ mode: 'cors',
+ body: `{"template":"${template}"}`,
+ });
+ if (res.status == 200) {
+ markers.push({
+ message: await res.text(),
+ severity: monaco.MarkerSeverity.Info,
+ ...range,
+ startColumn: trim,
+ });
+ } else if (res.status == 400) {
+ markers.push({
+ message: (await res.json()).message,
+ severity: monaco.MarkerSeverity.Error,
+ ...range,
+ startColumn: trim,
+ });
+ } else if (res.status == 401 || res.status == 403) {
+ document.querySelector('#api_token').classList.add('invalid');
+ } else {
+ markers.push({
+ message: res.statusText,
+ severity: monaco.MarkerSeverity.Error,
+ ...range,
+ startColumn: trim,
+ });
+ }
+ }
+ const templates = [];
+ /**
+ * @param {import('json-ast-comments').JsonAst | import('json-ast-comments').JsonProperty} node
+ * @param {string[]} path
+ */
+ function recurse(node, path) {
+ if (node.type === 'property') {
+ if (node.key[0].value === 'content') {
+ templates.push([
+ {
+ startLineNumber: node.key[0].range.start.line + 1,
+ startColumn: 0,
+ endLineNumber: node.value[0].range.end.line + 1,
+ endColumn: 10000,
+ },
+ node.value[0].value,
+ ]);
+ } else if (entities != null && node.key[0].value === 'entity') {
+ const range = {
+ startLineNumber: node.key[0].range.start.line + 1,
+ startColumn: 0,
+ endLineNumber: node.value[0].range.end.line + 1,
+ endColumn: 10000,
+ };
+ const l = model.getValueInRange(range);
+ let trim = 0;
+ while (trim < l.length && l[trim] == ' ') {
+ trim++;
+ }
+ trim++;
+ markers.push({
+ message: entities[node.value[0].value] ?? 'Entity not found',
+ severity: monaco.MarkerSeverity.Hint,
+ ...range,
+ startColumn: trim,
+ });
+ } else if (node.key[0].value === 'type') {
+ if (node.value[0].value === 'toggle') {
+ toggles.push([
+ {
+ startLineNumber: node.key[0].range.start.line + 1,
+ startColumn: 0,
+ endLineNumber: node.value[0].range.end.line + 1,
+ endColumn: 10000,
+ },
+ get(data, path).entity,
+ ]);
+ } else {
+ glyphs.push({
+ range: {
+ startLineNumber: node.key[0].range.start.line + 1,
+ startColumn: 0,
+ endLineNumber: node.value[0].range.end.line + 1,
+ endColumn: 10000,
+ },
+ options: {
+ isWholeLine: true,
+ glyphMarginClassName: node.value[0].value,
+ },
+ });
+ }
+ } else {
+ recurse(node.value[0], [...path, node.key[0].value]);
+ }
+ } else if (node.type === 'array') {
+ for (let i = 0; i < node.members.length; i++) {
+ recurse(node.members[i], [...path, i]);
+ }
+ } else if (node.type === 'object') {
+ for (let member of node.members) {
+ recurse(member, path);
+ }
+ }
+ }
+ recurse(ast.body[0], []);
+ decorations.clear();
+ decorations.append(glyphs);
+ await Promise.all(templates.map((t) => testTemplate(...t)));
+ toggles.forEach((t) => testToggle(...t));
+ monaco.editor.setModelMarkers(model, 'template', markers);
+ } catch {}
+ }
+ makeMarkers();
+
+ model.onDidChangeContent(async function () {
+ localStorage.setItem('json', model.getValue());
+ makeMarkers();
+ });
+
+ monaco.languages.registerCodeLensProvider('json', {
+ provideCodeLenses: function (model, token) {
+ const lenses = [];
+ try {
+ const ast = json.parse(model.getValue());
+ const data = JSON.parse(model.getValue());
+ /**
+ * @param {import('json-ast-comments').JsonAst | import('json-ast-comments').JsonProperty} node
+ * @param {string[]} path
+ */
+ function recurse(node, path) {
+ if (node.type === 'property') {
+ if (node.key[0].value === 'tap_action') {
+ const d = get(data, path);
+ if (d.tap_action.service) {
+ lenses.push({
+ range: {
+ startLineNumber: node.key[0].range.start.line + 1,
+ startColumn: 0,
+ endLineNumber: node.key[0].range.start.line + 1,
+ endColumn: 0,
+ },
+ id: Math.random().toString(36).substring(7),
+ command: {
+ id: runAction,
+ title: 'Run Action',
+ arguments: [d],
+ },
+ });
+ } else {
+ recurse(node.value[0], [...path, node.key[0].value]);
+ }
+ } else if (node.key[0].value === 'content') {
+ lenses.push({
+ range: {
+ startLineNumber: node.key[0].range.start.line + 1,
+ startColumn: 0,
+ endLineNumber: node.key[0].range.start.line + 1,
+ endColumn: 0,
+ },
+ id: Math.random().toString(36).substring(7),
+ command: {
+ id: renderTemplate,
+ title: 'Render Template',
+ arguments: [node.value[0].value],
+ },
+ });
+ } else if (
+ node.key[0].value === 'type' &&
+ node.value[0].value === 'toggle'
+ ) {
+ lenses.push({
+ range: {
+ startLineNumber: node.key[0].range.start.line + 1,
+ startColumn: 0,
+ endLineNumber: node.key[0].range.start.line + 1,
+ endColumn: 0,
+ },
+ id: Math.random().toString(36).substring(7),
+ command: {
+ id: toggle,
+ title: 'Toggle',
+ arguments: [get(data, path)],
+ },
+ });
+ } else {
+ recurse(node.value[0], [...path, node.key[0].value]);
+ }
+ } else if (node.type === 'array') {
+ for (let i = 0; i < node.members.length; i++) {
+ recurse(node.members[i], [...path, i]);
+ }
+ } else if (node.type === 'object') {
+ for (let member of node.members) {
+ recurse(member, path);
+ }
+ }
+ }
+ recurse(ast.body[0], []);
+ } catch {}
+ return {
+ lenses,
+ dispose: () => {},
+ };
+ },
+ resolveCodeLens: function (model, codeLens, token) {
+ return codeLens;
+ },
+ });
+});
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..dd5300c
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "web",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "start": "serve .."
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "devDependencies": {
+ "@types/toastify-js": "^1.12.3",
+ "@vscode/webview-ui-toolkit": "1.4.0",
+ "json-ast-comments": "1.1.1",
+ "monaco-editor": "0.45.0",
+ "serve": "^14.2.1"
+ }
+}
\ No newline at end of file
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
new file mode 100644
index 0000000..ec6251d
--- /dev/null
+++ b/web/pnpm-lock.yaml
@@ -0,0 +1,676 @@
+lockfileVersion: '6.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+devDependencies:
+ '@types/toastify-js':
+ specifier: ^1.12.3
+ version: 1.12.3
+ '@vscode/webview-ui-toolkit':
+ specifier: 1.4.0
+ version: 1.4.0(react@18.2.0)
+ json-ast-comments:
+ specifier: 1.1.1
+ version: 1.1.1
+ monaco-editor:
+ specifier: 0.45.0
+ version: 0.45.0
+ serve:
+ specifier: ^14.2.1
+ version: 14.2.1
+
+packages:
+
+ /@microsoft/fast-element@1.12.0:
+ resolution: {integrity: sha512-gQutuDHPKNxUEcQ4pypZT4Wmrbapus+P9s3bR/SEOLsMbNqNoXigGImITygI5zhb+aA5rzflM6O8YWkmRbGkPA==}
+ dev: true
+
+ /@microsoft/fast-foundation@2.49.4:
+ resolution: {integrity: sha512-5I2tSPo6bnOfVAIX7XzX+LhilahwvD7h+yzl3jW0t5IYmMX9Lci9VUVyx5f8hHdb1O9a8Y9Atb7Asw7yFO/u+w==}
+ dependencies:
+ '@microsoft/fast-element': 1.12.0
+ '@microsoft/fast-web-utilities': 5.4.1
+ tabbable: 5.3.3
+ tslib: 1.14.1
+ dev: true
+
+ /@microsoft/fast-react-wrapper@0.3.22(react@18.2.0):
+ resolution: {integrity: sha512-XhlX4m6znh7XW92oPvlKoG9USUn9JtF9rP1qtUoIbkaDaFtUS+H8o1Jn6/oK/rS44LbBLJXrvRkInmSWlDiGFw==}
+ peerDependencies:
+ react: '>=16.9.0'
+ dependencies:
+ '@microsoft/fast-element': 1.12.0
+ '@microsoft/fast-foundation': 2.49.4
+ react: 18.2.0
+ dev: true
+
+ /@microsoft/fast-web-utilities@5.4.1:
+ resolution: {integrity: sha512-ReWYncndjV3c8D8iq9tp7NcFNc1vbVHvcBFPME2nNFKNbS1XCesYZGlIlf3ot5EmuOXPlrzUHOWzQ2vFpIkqDg==}
+ dependencies:
+ exenv-es6: 1.1.1
+ dev: true
+
+ /@types/toastify-js@1.12.3:
+ resolution: {integrity: sha512-9RjLlbAHMSaae/KZNHGv19VG4gcLIm3YjvacCXBtfMfYn26h76YP5oxXI8k26q4iKXCB9LNfv18lsoS0JnFPTg==}
+ dev: true
+
+ /@vscode/webview-ui-toolkit@1.4.0(react@18.2.0):
+ resolution: {integrity: sha512-modXVHQkZLsxgmd5yoP3ptRC/G8NBDD+ob+ngPiWNQdlrH6H1xR/qgOBD85bfU3BhOB5sZzFWBwwhp9/SfoHww==}
+ peerDependencies:
+ react: '>=16.9.0'
+ dependencies:
+ '@microsoft/fast-element': 1.12.0
+ '@microsoft/fast-foundation': 2.49.4
+ '@microsoft/fast-react-wrapper': 0.3.22(react@18.2.0)
+ react: 18.2.0
+ tslib: 2.6.2
+ dev: true
+
+ /@zeit/schemas@2.29.0:
+ resolution: {integrity: sha512-g5QiLIfbg3pLuYUJPlisNKY+epQJTcMDsOnVNkscrDP1oi7vmJnzOANYJI/1pZcVJ6umUkBv3aFtlg1UvUHGzA==}
+ dev: true
+
+ /accepts@1.3.8:
+ resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
+ engines: {node: '>= 0.6'}
+ dependencies:
+ mime-types: 2.1.35
+ negotiator: 0.6.3
+ dev: true
+
+ /ajv@8.11.0:
+ resolution: {integrity: sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==}
+ dependencies:
+ fast-deep-equal: 3.1.3
+ json-schema-traverse: 1.0.0
+ require-from-string: 2.0.2
+ uri-js: 4.4.1
+ dev: true
+
+ /ansi-align@3.0.1:
+ resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
+ dependencies:
+ string-width: 4.2.3
+ dev: true
+
+ /ansi-regex@5.0.1:
+ resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /ansi-regex@6.0.1:
+ resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /ansi-styles@4.3.0:
+ resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+ engines: {node: '>=8'}
+ dependencies:
+ color-convert: 2.0.1
+ dev: true
+
+ /ansi-styles@6.2.1:
+ resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
+ engines: {node: '>=12'}
+ dev: true
+
+ /arch@2.2.0:
+ resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==}
+ dev: true
+
+ /arg@5.0.2:
+ resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
+ dev: true
+
+ /balanced-match@1.0.2:
+ resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+ dev: true
+
+ /boxen@7.0.0:
+ resolution: {integrity: sha512-j//dBVuyacJbvW+tvZ9HuH03fZ46QcaKvvhZickZqtB271DxJ7SNRSNxrV/dZX0085m7hISRZWbzWlJvx/rHSg==}
+ engines: {node: '>=14.16'}
+ dependencies:
+ ansi-align: 3.0.1
+ camelcase: 7.0.1
+ chalk: 5.0.1
+ cli-boxes: 3.0.0
+ string-width: 5.1.2
+ type-fest: 2.19.0
+ widest-line: 4.0.1
+ wrap-ansi: 8.1.0
+ dev: true
+
+ /brace-expansion@1.1.11:
+ resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==}
+ dependencies:
+ balanced-match: 1.0.2
+ concat-map: 0.0.1
+ dev: true
+
+ /bytes@3.0.0:
+ resolution: {integrity: sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==}
+ engines: {node: '>= 0.8'}
+ dev: true
+
+ /camelcase@7.0.1:
+ resolution: {integrity: sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==}
+ engines: {node: '>=14.16'}
+ dev: true
+
+ /chalk-template@0.4.0:
+ resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==}
+ engines: {node: '>=12'}
+ dependencies:
+ chalk: 4.1.2
+ dev: true
+
+ /chalk@4.1.2:
+ resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+ engines: {node: '>=10'}
+ dependencies:
+ ansi-styles: 4.3.0
+ supports-color: 7.2.0
+ dev: true
+
+ /chalk@5.0.1:
+ resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==}
+ engines: {node: ^12.17.0 || ^14.13 || >=16.0.0}
+ dev: true
+
+ /cli-boxes@3.0.0:
+ resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /clipboardy@3.0.0:
+ resolution: {integrity: sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dependencies:
+ arch: 2.2.0
+ execa: 5.1.1
+ is-wsl: 2.2.0
+ dev: true
+
+ /color-convert@2.0.1:
+ resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+ engines: {node: '>=7.0.0'}
+ dependencies:
+ color-name: 1.1.4
+ dev: true
+
+ /color-name@1.1.4:
+ resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+ dev: true
+
+ /compressible@2.0.18:
+ resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
+ engines: {node: '>= 0.6'}
+ dependencies:
+ mime-db: 1.52.0
+ dev: true
+
+ /compression@1.7.4:
+ resolution: {integrity: sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==}
+ engines: {node: '>= 0.8.0'}
+ dependencies:
+ accepts: 1.3.8
+ bytes: 3.0.0
+ compressible: 2.0.18
+ debug: 2.6.9
+ on-headers: 1.0.2
+ safe-buffer: 5.1.2
+ vary: 1.1.2
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /concat-map@0.0.1:
+ resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+ dev: true
+
+ /content-disposition@0.5.2:
+ resolution: {integrity: sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==}
+ engines: {node: '>= 0.6'}
+ dev: true
+
+ /cross-spawn@7.0.3:
+ resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
+ engines: {node: '>= 8'}
+ dependencies:
+ path-key: 3.1.1
+ shebang-command: 2.0.0
+ which: 2.0.2
+ dev: true
+
+ /debug@2.6.9:
+ resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+ dependencies:
+ ms: 2.0.0
+ dev: true
+
+ /deep-extend@0.6.0:
+ resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
+ engines: {node: '>=4.0.0'}
+ dev: true
+
+ /eastasianwidth@0.2.0:
+ resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
+ dev: true
+
+ /emoji-regex@8.0.0:
+ resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
+ dev: true
+
+ /emoji-regex@9.2.2:
+ resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+ dev: true
+
+ /execa@5.1.1:
+ resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
+ engines: {node: '>=10'}
+ dependencies:
+ cross-spawn: 7.0.3
+ get-stream: 6.0.1
+ human-signals: 2.1.0
+ is-stream: 2.0.1
+ merge-stream: 2.0.0
+ npm-run-path: 4.0.1
+ onetime: 5.1.2
+ signal-exit: 3.0.7
+ strip-final-newline: 2.0.0
+ dev: true
+
+ /exenv-es6@1.1.1:
+ resolution: {integrity: sha512-vlVu3N8d6yEMpMsEm+7sUBAI81aqYYuEvfK0jNqmdb/OPXzzH7QWDDnVjMvDSY47JdHEqx/dfC/q8WkfoTmpGQ==}
+ dev: true
+
+ /fast-deep-equal@3.1.3:
+ resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+ dev: true
+
+ /fast-url-parser@1.1.3:
+ resolution: {integrity: sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==}
+ dependencies:
+ punycode: 1.4.1
+ dev: true
+
+ /get-stream@6.0.1:
+ resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
+ engines: {node: '>=10'}
+ dev: true
+
+ /has-flag@4.0.0:
+ resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /human-signals@2.1.0:
+ resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
+ engines: {node: '>=10.17.0'}
+ dev: true
+
+ /ini@1.3.8:
+ resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
+ dev: true
+
+ /is-docker@2.2.1:
+ resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
+ engines: {node: '>=8'}
+ hasBin: true
+ dev: true
+
+ /is-fullwidth-code-point@3.0.0:
+ resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /is-port-reachable@4.0.0:
+ resolution: {integrity: sha512-9UoipoxYmSk6Xy7QFgRv2HDyaysmgSG75TFQs6S+3pDM7ZhKTF/bskZV+0UlABHzKjNVhPjYCLfeZUEg1wXxig==}
+ engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ dev: true
+
+ /is-stream@2.0.1:
+ resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /is-wsl@2.2.0:
+ resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
+ engines: {node: '>=8'}
+ dependencies:
+ is-docker: 2.2.1
+ dev: true
+
+ /isexe@2.0.0:
+ resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+ dev: true
+
+ /js-tokens@4.0.0:
+ resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+ dev: true
+
+ /json-ast-comments@1.1.1:
+ resolution: {integrity: sha512-UOHlf7ns5t1GiI3+T5tf9SN2OepXTo/sqhd+cQj++DaUMKQOOCbX+eRlIoHcEv9m902reDTc1mCJD4J69xFJSg==}
+ dev: true
+
+ /json-schema-traverse@1.0.0:
+ resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
+ dev: true
+
+ /loose-envify@1.4.0:
+ resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+ hasBin: true
+ dependencies:
+ js-tokens: 4.0.0
+ dev: true
+
+ /merge-stream@2.0.0:
+ resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
+ dev: true
+
+ /mime-db@1.33.0:
+ resolution: {integrity: sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==}
+ engines: {node: '>= 0.6'}
+ dev: true
+
+ /mime-db@1.52.0:
+ resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+ engines: {node: '>= 0.6'}
+ dev: true
+
+ /mime-types@2.1.18:
+ resolution: {integrity: sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==}
+ engines: {node: '>= 0.6'}
+ dependencies:
+ mime-db: 1.33.0
+ dev: true
+
+ /mime-types@2.1.35:
+ resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+ engines: {node: '>= 0.6'}
+ dependencies:
+ mime-db: 1.52.0
+ dev: true
+
+ /mimic-fn@2.1.0:
+ resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /minimatch@3.1.2:
+ resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+ dependencies:
+ brace-expansion: 1.1.11
+ dev: true
+
+ /minimist@1.2.8:
+ resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+ dev: true
+
+ /monaco-editor@0.45.0:
+ resolution: {integrity: sha512-mjv1G1ZzfEE3k9HZN0dQ2olMdwIfaeAAjFiwNprLfYNRSz7ctv9XuCT7gPtBGrMUeV1/iZzYKj17Khu1hxoHOA==}
+ dev: true
+
+ /ms@2.0.0:
+ resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
+ dev: true
+
+ /negotiator@0.6.3:
+ resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
+ engines: {node: '>= 0.6'}
+ dev: true
+
+ /npm-run-path@4.0.1:
+ resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
+ engines: {node: '>=8'}
+ dependencies:
+ path-key: 3.1.1
+ dev: true
+
+ /on-headers@1.0.2:
+ resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==}
+ engines: {node: '>= 0.8'}
+ dev: true
+
+ /onetime@5.1.2:
+ resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
+ engines: {node: '>=6'}
+ dependencies:
+ mimic-fn: 2.1.0
+ dev: true
+
+ /path-is-inside@1.0.2:
+ resolution: {integrity: sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==}
+ dev: true
+
+ /path-key@3.1.1:
+ resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /path-to-regexp@2.2.1:
+ resolution: {integrity: sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==}
+ dev: true
+
+ /punycode@1.4.1:
+ resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
+ dev: true
+
+ /punycode@2.3.1:
+ resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /range-parser@1.2.0:
+ resolution: {integrity: sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==}
+ engines: {node: '>= 0.6'}
+ dev: true
+
+ /rc@1.2.8:
+ resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
+ hasBin: true
+ dependencies:
+ deep-extend: 0.6.0
+ ini: 1.3.8
+ minimist: 1.2.8
+ strip-json-comments: 2.0.1
+ dev: true
+
+ /react@18.2.0:
+ resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ loose-envify: 1.4.0
+ dev: true
+
+ /registry-auth-token@3.3.2:
+ resolution: {integrity: sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==}
+ dependencies:
+ rc: 1.2.8
+ safe-buffer: 5.2.1
+ dev: true
+
+ /registry-url@3.1.0:
+ resolution: {integrity: sha512-ZbgR5aZEdf4UKZVBPYIgaglBmSF2Hi94s2PcIHhRGFjKYu+chjJdYfHn4rt3hB6eCKLJ8giVIIfgMa1ehDfZKA==}
+ engines: {node: '>=0.10.0'}
+ dependencies:
+ rc: 1.2.8
+ dev: true
+
+ /require-from-string@2.0.2:
+ resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /safe-buffer@5.1.2:
+ resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
+ dev: true
+
+ /safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+ dev: true
+
+ /serve-handler@6.1.5:
+ resolution: {integrity: sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==}
+ dependencies:
+ bytes: 3.0.0
+ content-disposition: 0.5.2
+ fast-url-parser: 1.1.3
+ mime-types: 2.1.18
+ minimatch: 3.1.2
+ path-is-inside: 1.0.2
+ path-to-regexp: 2.2.1
+ range-parser: 1.2.0
+ dev: true
+
+ /serve@14.2.1:
+ resolution: {integrity: sha512-48er5fzHh7GCShLnNyPBRPEjs2I6QBozeGr02gaacROiyS/8ARADlj595j39iZXAqBbJHH/ivJJyPRWY9sQWZA==}
+ engines: {node: '>= 14'}
+ hasBin: true
+ dependencies:
+ '@zeit/schemas': 2.29.0
+ ajv: 8.11.0
+ arg: 5.0.2
+ boxen: 7.0.0
+ chalk: 5.0.1
+ chalk-template: 0.4.0
+ clipboardy: 3.0.0
+ compression: 1.7.4
+ is-port-reachable: 4.0.0
+ serve-handler: 6.1.5
+ update-check: 1.5.4
+ transitivePeerDependencies:
+ - supports-color
+ dev: true
+
+ /shebang-command@2.0.0:
+ resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+ engines: {node: '>=8'}
+ dependencies:
+ shebang-regex: 3.0.0
+ dev: true
+
+ /shebang-regex@3.0.0:
+ resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+ engines: {node: '>=8'}
+ dev: true
+
+ /signal-exit@3.0.7:
+ resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==}
+ dev: true
+
+ /string-width@4.2.3:
+ resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
+ engines: {node: '>=8'}
+ dependencies:
+ emoji-regex: 8.0.0
+ is-fullwidth-code-point: 3.0.0
+ strip-ansi: 6.0.1
+ dev: true
+
+ /string-width@5.1.2:
+ resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
+ engines: {node: '>=12'}
+ dependencies:
+ eastasianwidth: 0.2.0
+ emoji-regex: 9.2.2
+ strip-ansi: 7.1.0
+ dev: true
+
+ /strip-ansi@6.0.1:
+ resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
+ engines: {node: '>=8'}
+ dependencies:
+ ansi-regex: 5.0.1
+ dev: true
+
+ /strip-ansi@7.1.0:
+ resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ ansi-regex: 6.0.1
+ dev: true
+
+ /strip-final-newline@2.0.0:
+ resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
+ engines: {node: '>=6'}
+ dev: true
+
+ /strip-json-comments@2.0.1:
+ resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
+ engines: {node: '>=0.10.0'}
+ dev: true
+
+ /supports-color@7.2.0:
+ resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+ engines: {node: '>=8'}
+ dependencies:
+ has-flag: 4.0.0
+ dev: true
+
+ /tabbable@5.3.3:
+ resolution: {integrity: sha512-QD9qKY3StfbZqWOPLp0++pOrAVb/HbUi5xCc8cUo4XjP19808oaMiDzn0leBY5mCespIBM0CIZePzZjgzR83kA==}
+ dev: true
+
+ /tslib@1.14.1:
+ resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
+ dev: true
+
+ /tslib@2.6.2:
+ resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
+ dev: true
+
+ /type-fest@2.19.0:
+ resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
+ engines: {node: '>=12.20'}
+ dev: true
+
+ /update-check@1.5.4:
+ resolution: {integrity: sha512-5YHsflzHP4t1G+8WGPlvKbJEbAJGCgw+Em+dGR1KmBUbr1J36SJBqlHLjR7oob7sco5hWHGQVcr9B2poIVDDTQ==}
+ dependencies:
+ registry-auth-token: 3.3.2
+ registry-url: 3.1.0
+ dev: true
+
+ /uri-js@4.4.1:
+ resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+ dependencies:
+ punycode: 2.3.1
+ dev: true
+
+ /vary@1.1.2:
+ resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==}
+ engines: {node: '>= 0.8'}
+ dev: true
+
+ /which@2.0.2:
+ resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+ engines: {node: '>= 8'}
+ hasBin: true
+ dependencies:
+ isexe: 2.0.0
+ dev: true
+
+ /widest-line@4.0.1:
+ resolution: {integrity: sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==}
+ engines: {node: '>=12'}
+ dependencies:
+ string-width: 5.1.2
+ dev: true
+
+ /wrap-ansi@8.1.0:
+ resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
+ engines: {node: '>=12'}
+ dependencies:
+ ansi-styles: 6.2.1
+ string-width: 5.1.2
+ strip-ansi: 7.1.0
+ dev: true
diff --git a/web/types.d.ts b/web/types.d.ts
new file mode 100644
index 0000000..a5d4f97
--- /dev/null
+++ b/web/types.d.ts
@@ -0,0 +1,3 @@
+declare namespace json {
+ export function parse(text: string): import('json-ast-comments').JsonDocument;
+}