From fc4c42e20a734aed41bb8c755c10a7cc14743f1d Mon Sep 17 00:00:00 2001 From: Vikalp Paliwal Date: Thu, 13 Aug 2026 11:11:06 +0530 Subject: [PATCH] Wire staff KDS, customer cart UX, and staff inventory voice AI. Staff portal and customer flow now use live APIs for KDS tickets, cart/order status, and low-stock voice assistance. Co-authored-by: Cursor --- .dockerignore | 5 + .env.example | 10 +- Dockerfile | 12 + dev-dist/sw.js | 109 + dev-dist/workbox-5ccb27be.js | 4695 ++++++++++++++++ index.html | 12 +- package-lock.json | 4813 ++++++++++++++++- package.json | 6 +- packages/ui/package.json | 18 + packages/ui/src/StatusBadge.tsx | 117 + packages/ui/src/index.ts | 3 + packages/ui/src/theme.ts | 146 + src/App.tsx | 335 +- src/apps/customer/App.tsx | 441 ++ src/apps/customer/main.tsx | 17 + src/apps/staff/App.tsx | 379 ++ src/apps/staff/main.tsx | 11 + src/apps/staff/staff.css | 23 + src/components/admin/AdminDesktopShell.tsx | 246 +- src/components/admin/FeedbackInboxView.tsx | 28 +- src/components/admin/InventoryView.tsx | 19 +- src/components/admin/LowStockNotifier.tsx | 114 + src/components/admin/MenuManagementView.tsx | 373 ++ src/components/admin/StaffLoginModal.tsx | 93 +- src/components/admin/StaffShiftsView.tsx | 2 +- src/components/admin/SuppliersView.tsx | 11 +- src/components/common/StatusBadge.tsx | 104 +- src/components/customer/BillPaymentView.tsx | 419 +- src/components/customer/CartPeekDrawer.tsx | 234 + src/components/customer/CartReviewView.tsx | 248 +- .../customer/CustomizationModal.tsx | 4 +- src/components/customer/MenuBrowseView.tsx | 403 +- src/components/customer/OrderStatusView.tsx | 99 +- src/components/customer/TableLandingView.tsx | 191 +- .../customer/VoiceAssistantModal.tsx | 468 +- src/components/kds/KDSKanban.tsx | 63 +- src/components/kds/KDSTicketCard.tsx | 164 +- src/components/voice/VoiceOrderingView.tsx | 497 +- src/data/menuData.ts | 56 +- src/hooks/useSpeechRecognition.ts | 239 +- src/hooks/useSpeechSynthesis.ts | 28 +- src/i18n/LocaleContext.tsx | 59 + src/i18n/messages.ts | 134 + src/main.tsx | 12 +- src/services/api.ts | 494 +- src/theme/theme.ts | 147 +- src/utils/debugLog.ts | 46 + src/vite-env.d.ts | 2 + staff.html | 18 + tsconfig.app.json | 12 +- vite.config.ts | 87 +- 51 files changed, 14517 insertions(+), 1749 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 dev-dist/sw.js create mode 100644 dev-dist/workbox-5ccb27be.js create mode 100644 packages/ui/package.json create mode 100644 packages/ui/src/StatusBadge.tsx create mode 100644 packages/ui/src/index.ts create mode 100644 packages/ui/src/theme.ts create mode 100644 src/apps/customer/App.tsx create mode 100644 src/apps/customer/main.tsx create mode 100644 src/apps/staff/App.tsx create mode 100644 src/apps/staff/main.tsx create mode 100644 src/apps/staff/staff.css create mode 100644 src/components/admin/LowStockNotifier.tsx create mode 100644 src/components/admin/MenuManagementView.tsx create mode 100644 src/components/customer/CartPeekDrawer.tsx create mode 100644 src/i18n/LocaleContext.tsx create mode 100644 src/i18n/messages.ts create mode 100644 src/utils/debugLog.ts create mode 100644 src/vite-env.d.ts create mode 100644 staff.html diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..696680d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +node_modules +dist +.git +graphify-out +.env diff --git a/.env.example b/.env.example index cebb702..eff5710 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,9 @@ # RestroAI Frontend Environment Variables -# The HTTP URL of the remote backend server -VITE_API_URL=https://api.yourdomain.com +# Local full stack (docker compose up from retro_backend repo) +VITE_API_URL=http://localhost:8000 +VITE_WS_URL=ws://localhost:8000 -# The WebSocket URL of the remote backend server -VITE_WS_URL=wss://api.yourdomain.com +# Production +# VITE_API_URL=https://restro-backend.navigolabs.com +# VITE_WS_URL=wss://restro-backend.navigolabs.com diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..adaef5f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM node:20-alpine + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . + +EXPOSE 5173 + +CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0", "--port", "5173"] diff --git a/dev-dist/sw.js b/dev-dist/sw.js new file mode 100644 index 0000000..59965b3 --- /dev/null +++ b/dev-dist/sw.js @@ -0,0 +1,109 @@ +/** + * Copyright 2018 Google Inc. All Rights Reserved. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// If the loader is already loaded, just stop. +if (!self.define) { + let registry = {}; + + // Used for `eval` and `importScripts` where we can't get script URL by other means. + // In both cases, it's safe to use a global var because those functions are synchronous. + let nextDefineUri; + + const singleRequire = (uri, parentUri) => { + uri = new URL(uri + ".js", parentUri).href; + return registry[uri] || ( + + new Promise(resolve => { + if ("document" in self) { + const script = document.createElement("script"); + script.src = uri; + script.onload = resolve; + document.head.appendChild(script); + } else { + nextDefineUri = uri; + importScripts(uri); + resolve(); + } + }) + + .then(() => { + let promise = registry[uri]; + if (!promise) { + throw new Error(`Module ${uri} didn’t register its module`); + } + return promise; + }) + ); + }; + + self.define = (depsNames, factory) => { + const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href; + if (registry[uri]) { + // Module is already loading or loaded. + return; + } + let exports = {}; + const require = depUri => singleRequire(depUri, uri); + const specialDeps = { + module: { uri }, + exports, + require + }; + registry[uri] = Promise.all(depsNames.map( + depName => specialDeps[depName] || require(depName) + )).then(deps => { + factory(...deps); + return exports; + }); + }; +} +define(['./workbox-5ccb27be'], (function (workbox) { 'use strict'; + + self.skipWaiting(); + workbox.clientsClaim(); + /** + * The precacheAndRoute() method efficiently caches and responds to + * requests for URLs in the manifest. + * See https://goo.gl/S9QRab + */ + workbox.precacheAndRoute([{ + "url": "/index.html", + "revision": "0.fhp5592siig" + }], {}); + workbox.cleanupOutdatedCaches(); + workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("/index.html"), { + allowlist: [/^\/$/], + denylist: [/^\/staff\.html/, /^\/api/] + })); + workbox.registerRoute(({ + url + }) => url.pathname.includes("/menu/public"), new workbox.StaleWhileRevalidate({ + "cacheName": "restroai-menu", + plugins: [new workbox.ExpirationPlugin({ + maxEntries: 20, + maxAgeSeconds: 86400 + }), new workbox.CacheableResponsePlugin({ + statuses: [0, 200] + })] + }), 'GET'); + workbox.registerRoute(({ + request + }) => request.destination === "image", new workbox.CacheFirst({ + "cacheName": "restroai-images", + plugins: [new workbox.ExpirationPlugin({ + maxEntries: 60, + maxAgeSeconds: 604800 + })] + }), 'GET'); + +})); diff --git a/dev-dist/workbox-5ccb27be.js b/dev-dist/workbox-5ccb27be.js new file mode 100644 index 0000000..571e5d3 --- /dev/null +++ b/dev-dist/workbox-5ccb27be.js @@ -0,0 +1,4695 @@ +define(['exports'], (function (exports) { 'use strict'; + + // @ts-ignore + try { + self['workbox:core:7.4.0'] && _(); + } catch (e) {} + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const logger = (() => { + // Don't overwrite this value if it's already set. + // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923 + if (!('__WB_DISABLE_DEV_LOGS' in globalThis)) { + self.__WB_DISABLE_DEV_LOGS = false; + } + let inGroup = false; + const methodToColorMap = { + debug: `#7f8c8d`, + log: `#2ecc71`, + warn: `#f39c12`, + error: `#c0392b`, + groupCollapsed: `#3498db`, + groupEnd: null // No colored prefix on groupEnd + }; + const print = function (method, args) { + if (self.__WB_DISABLE_DEV_LOGS) { + return; + } + if (method === 'groupCollapsed') { + // Safari doesn't print all console.groupCollapsed() arguments: + // https://bugs.webkit.org/show_bug.cgi?id=182754 + if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { + console[method](...args); + return; + } + } + const styles = [`background: ${methodToColorMap[method]}`, `border-radius: 0.5em`, `color: white`, `font-weight: bold`, `padding: 2px 0.5em`]; + // When in a group, the workbox prefix is not displayed. + const logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')]; + console[method](...logPrefix, ...args); + if (method === 'groupCollapsed') { + inGroup = true; + } + if (method === 'groupEnd') { + inGroup = false; + } + }; + // eslint-disable-next-line @typescript-eslint/ban-types + const api = {}; + const loggerMethods = Object.keys(methodToColorMap); + for (const key of loggerMethods) { + const method = key; + api[method] = (...args) => { + print(method, args); + }; + } + return api; + })(); + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const messages$1 = { + 'invalid-value': ({ + paramName, + validValueDescription, + value + }) => { + if (!paramName || !validValueDescription) { + throw new Error(`Unexpected input to 'invalid-value' error.`); + } + return `The '${paramName}' parameter was given a value with an ` + `unexpected value. ${validValueDescription} Received a value of ` + `${JSON.stringify(value)}.`; + }, + 'not-an-array': ({ + moduleName, + className, + funcName, + paramName + }) => { + if (!moduleName || !className || !funcName || !paramName) { + throw new Error(`Unexpected input to 'not-an-array' error.`); + } + return `The parameter '${paramName}' passed into ` + `'${moduleName}.${className}.${funcName}()' must be an array.`; + }, + 'incorrect-type': ({ + expectedType, + paramName, + moduleName, + className, + funcName + }) => { + if (!expectedType || !paramName || !moduleName || !funcName) { + throw new Error(`Unexpected input to 'incorrect-type' error.`); + } + const classNameStr = className ? `${className}.` : ''; + return `The parameter '${paramName}' passed into ` + `'${moduleName}.${classNameStr}` + `${funcName}()' must be of type ${expectedType}.`; + }, + 'incorrect-class': ({ + expectedClassName, + paramName, + moduleName, + className, + funcName, + isReturnValueProblem + }) => { + if (!expectedClassName || !moduleName || !funcName) { + throw new Error(`Unexpected input to 'incorrect-class' error.`); + } + const classNameStr = className ? `${className}.` : ''; + if (isReturnValueProblem) { + return `The return value from ` + `'${moduleName}.${classNameStr}${funcName}()' ` + `must be an instance of class ${expectedClassName}.`; + } + return `The parameter '${paramName}' passed into ` + `'${moduleName}.${classNameStr}${funcName}()' ` + `must be an instance of class ${expectedClassName}.`; + }, + 'missing-a-method': ({ + expectedMethod, + paramName, + moduleName, + className, + funcName + }) => { + if (!expectedMethod || !paramName || !moduleName || !className || !funcName) { + throw new Error(`Unexpected input to 'missing-a-method' error.`); + } + return `${moduleName}.${className}.${funcName}() expected the ` + `'${paramName}' parameter to expose a '${expectedMethod}' method.`; + }, + 'add-to-cache-list-unexpected-type': ({ + entry + }) => { + return `An unexpected entry was passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' The entry ` + `'${JSON.stringify(entry)}' isn't supported. You must supply an array of ` + `strings with one or more characters, objects with a url property or ` + `Request objects.`; + }, + 'add-to-cache-list-conflicting-entries': ({ + firstEntry, + secondEntry + }) => { + if (!firstEntry || !secondEntry) { + throw new Error(`Unexpected input to ` + `'add-to-cache-list-duplicate-entries' error.`); + } + return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${firstEntry} but different revision details. Workbox is ` + `unable to cache and version the asset correctly. Please remove one ` + `of the entries.`; + }, + 'plugin-error-request-will-fetch': ({ + thrownErrorMessage + }) => { + if (!thrownErrorMessage) { + throw new Error(`Unexpected input to ` + `'plugin-error-request-will-fetch', error.`); + } + return `An error was thrown by a plugins 'requestWillFetch()' method. ` + `The thrown error message was: '${thrownErrorMessage}'.`; + }, + 'invalid-cache-name': ({ + cacheNameId, + value + }) => { + if (!cacheNameId) { + throw new Error(`Expected a 'cacheNameId' for error 'invalid-cache-name'`); + } + return `You must provide a name containing at least one character for ` + `setCacheDetails({${cacheNameId}: '...'}). Received a value of ` + `'${JSON.stringify(value)}'`; + }, + 'unregister-route-but-not-found-with-method': ({ + method + }) => { + if (!method) { + throw new Error(`Unexpected input to ` + `'unregister-route-but-not-found-with-method' error.`); + } + return `The route you're trying to unregister was not previously ` + `registered for the method type '${method}'.`; + }, + 'unregister-route-route-not-registered': () => { + return `The route you're trying to unregister was not previously ` + `registered.`; + }, + 'queue-replay-failed': ({ + name + }) => { + return `Replaying the background sync queue '${name}' failed.`; + }, + 'duplicate-queue-name': ({ + name + }) => { + return `The Queue name '${name}' is already being used. ` + `All instances of backgroundSync.Queue must be given unique names.`; + }, + 'expired-test-without-max-age': ({ + methodName, + paramName + }) => { + return `The '${methodName}()' method can only be used when the ` + `'${paramName}' is used in the constructor.`; + }, + 'unsupported-route-type': ({ + moduleName, + className, + funcName, + paramName + }) => { + return `The supplied '${paramName}' parameter was an unsupported type. ` + `Please check the docs for ${moduleName}.${className}.${funcName} for ` + `valid input types.`; + }, + 'not-array-of-class': ({ + value, + expectedClass, + moduleName, + className, + funcName, + paramName + }) => { + return `The supplied '${paramName}' parameter must be an array of ` + `'${expectedClass}' objects. Received '${JSON.stringify(value)},'. ` + `Please check the call to ${moduleName}.${className}.${funcName}() ` + `to fix the issue.`; + }, + 'max-entries-or-age-required': ({ + moduleName, + className, + funcName + }) => { + return `You must define either config.maxEntries or config.maxAgeSeconds` + `in ${moduleName}.${className}.${funcName}`; + }, + 'statuses-or-headers-required': ({ + moduleName, + className, + funcName + }) => { + return `You must define either config.statuses or config.headers` + `in ${moduleName}.${className}.${funcName}`; + }, + 'invalid-string': ({ + moduleName, + funcName, + paramName + }) => { + if (!paramName || !moduleName || !funcName) { + throw new Error(`Unexpected input to 'invalid-string' error.`); + } + return `When using strings, the '${paramName}' parameter must start with ` + `'http' (for cross-origin matches) or '/' (for same-origin matches). ` + `Please see the docs for ${moduleName}.${funcName}() for ` + `more info.`; + }, + 'channel-name-required': () => { + return `You must provide a channelName to construct a ` + `BroadcastCacheUpdate instance.`; + }, + 'invalid-responses-are-same-args': () => { + return `The arguments passed into responsesAreSame() appear to be ` + `invalid. Please ensure valid Responses are used.`; + }, + 'expire-custom-caches-only': () => { + return `You must provide a 'cacheName' property when using the ` + `expiration plugin with a runtime caching strategy.`; + }, + 'unit-must-be-bytes': ({ + normalizedRangeHeader + }) => { + if (!normalizedRangeHeader) { + throw new Error(`Unexpected input to 'unit-must-be-bytes' error.`); + } + return `The 'unit' portion of the Range header must be set to 'bytes'. ` + `The Range header provided was "${normalizedRangeHeader}"`; + }, + 'single-range-only': ({ + normalizedRangeHeader + }) => { + if (!normalizedRangeHeader) { + throw new Error(`Unexpected input to 'single-range-only' error.`); + } + return `Multiple ranges are not supported. Please use a single start ` + `value, and optional end value. The Range header provided was ` + `"${normalizedRangeHeader}"`; + }, + 'invalid-range-values': ({ + normalizedRangeHeader + }) => { + if (!normalizedRangeHeader) { + throw new Error(`Unexpected input to 'invalid-range-values' error.`); + } + return `The Range header is missing both start and end values. At least ` + `one of those values is needed. The Range header provided was ` + `"${normalizedRangeHeader}"`; + }, + 'no-range-header': () => { + return `No Range header was found in the Request provided.`; + }, + 'range-not-satisfiable': ({ + size, + start, + end + }) => { + return `The start (${start}) and end (${end}) values in the Range are ` + `not satisfiable by the cached response, which is ${size} bytes.`; + }, + 'attempt-to-cache-non-get-request': ({ + url, + method + }) => { + return `Unable to cache '${url}' because it is a '${method}' request and ` + `only 'GET' requests can be cached.`; + }, + 'cache-put-with-no-response': ({ + url + }) => { + return `There was an attempt to cache '${url}' but the response was not ` + `defined.`; + }, + 'no-response': ({ + url, + error + }) => { + let message = `The strategy could not generate a response for '${url}'.`; + if (error) { + message += ` The underlying error is ${error}.`; + } + return message; + }, + 'bad-precaching-response': ({ + url, + status + }) => { + return `The precaching request for '${url}' failed` + (status ? ` with an HTTP status of ${status}.` : `.`); + }, + 'non-precached-url': ({ + url + }) => { + return `createHandlerBoundToURL('${url}') was called, but that URL is ` + `not precached. Please pass in a URL that is precached instead.`; + }, + 'add-to-cache-list-conflicting-integrities': ({ + url + }) => { + return `Two of the entries passed to ` + `'workbox-precaching.PrecacheController.addToCacheList()' had the URL ` + `${url} with different integrity values. Please remove one of them.`; + }, + 'missing-precache-entry': ({ + cacheName, + url + }) => { + return `Unable to find a precached response in ${cacheName} for ${url}.`; + }, + 'cross-origin-copy-response': ({ + origin + }) => { + return `workbox-core.copyResponse() can only be used with same-origin ` + `responses. It was passed a response with origin ${origin}.`; + }, + 'opaque-streams-source': ({ + type + }) => { + const message = `One of the workbox-streams sources resulted in an ` + `'${type}' response.`; + if (type === 'opaqueredirect') { + return `${message} Please do not use a navigation request that results ` + `in a redirect as a source.`; + } + return `${message} Please ensure your sources are CORS-enabled.`; + } + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const generatorFunction = (code, details = {}) => { + const message = messages$1[code]; + if (!message) { + throw new Error(`Unable to find message for code '${code}'.`); + } + return message(details); + }; + const messageGenerator = generatorFunction; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Workbox errors should be thrown with this class. + * This allows use to ensure the type easily in tests, + * helps developers identify errors from workbox + * easily and allows use to optimise error + * messages correctly. + * + * @private + */ + class WorkboxError extends Error { + /** + * + * @param {string} errorCode The error code that + * identifies this particular error. + * @param {Object=} details Any relevant arguments + * that will help developers identify issues should + * be added as a key on the context object. + */ + constructor(errorCode, details) { + const message = messageGenerator(errorCode, details); + super(message); + this.name = errorCode; + this.details = details; + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /* + * This method throws if the supplied value is not an array. + * The destructed values are required to produce a meaningful error for users. + * The destructed and restructured object is so it's clear what is + * needed. + */ + const isArray = (value, details) => { + if (!Array.isArray(value)) { + throw new WorkboxError('not-an-array', details); + } + }; + const hasMethod = (object, expectedMethod, details) => { + const type = typeof object[expectedMethod]; + if (type !== 'function') { + details['expectedMethod'] = expectedMethod; + throw new WorkboxError('missing-a-method', details); + } + }; + const isType = (object, expectedType, details) => { + if (typeof object !== expectedType) { + details['expectedType'] = expectedType; + throw new WorkboxError('incorrect-type', details); + } + }; + const isInstance = (object, + // Need the general type to do the check later. + // eslint-disable-next-line @typescript-eslint/ban-types + expectedClass, details) => { + if (!(object instanceof expectedClass)) { + details['expectedClassName'] = expectedClass.name; + throw new WorkboxError('incorrect-class', details); + } + }; + const isOneOf = (value, validValues, details) => { + if (!validValues.includes(value)) { + details['validValueDescription'] = `Valid values are ${JSON.stringify(validValues)}.`; + throw new WorkboxError('invalid-value', details); + } + }; + const isArrayOfClass = (value, + // Need general type to do check later. + expectedClass, + // eslint-disable-line + details) => { + const error = new WorkboxError('not-array-of-class', details); + if (!Array.isArray(value)) { + throw error; + } + for (const item of value) { + if (!(item instanceof expectedClass)) { + throw error; + } + } + }; + const finalAssertExports = { + hasMethod, + isArray, + isInstance, + isOneOf, + isType, + isArrayOfClass + }; + + // @ts-ignore + try { + self['workbox:routing:7.4.0'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The default HTTP method, 'GET', used when there's no specific method + * configured for a route. + * + * @type {string} + * + * @private + */ + const defaultMethod = 'GET'; + /** + * The list of valid HTTP methods associated with requests that could be routed. + * + * @type {Array} + * + * @private + */ + const validMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT']; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * @param {function()|Object} handler Either a function, or an object with a + * 'handle' method. + * @return {Object} An object with a handle method. + * + * @private + */ + const normalizeHandler = handler => { + if (handler && typeof handler === 'object') { + { + finalAssertExports.hasMethod(handler, 'handle', { + moduleName: 'workbox-routing', + className: 'Route', + funcName: 'constructor', + paramName: 'handler' + }); + } + return handler; + } else { + { + finalAssertExports.isType(handler, 'function', { + moduleName: 'workbox-routing', + className: 'Route', + funcName: 'constructor', + paramName: 'handler' + }); + } + return { + handle: handler + }; + } + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A `Route` consists of a pair of callback functions, "match" and "handler". + * The "match" callback determine if a route should be used to "handle" a + * request by returning a non-falsy value if it can. The "handler" callback + * is called when there is a match and should return a Promise that resolves + * to a `Response`. + * + * @memberof workbox-routing + */ + class Route { + /** + * Constructor for Route class. + * + * @param {workbox-routing~matchCallback} match + * A callback function that determines whether the route matches a given + * `fetch` event by returning a non-falsy value. + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resolving to a Response. + * @param {string} [method='GET'] The HTTP method to match the Route + * against. + */ + constructor(match, handler, method = defaultMethod) { + { + finalAssertExports.isType(match, 'function', { + moduleName: 'workbox-routing', + className: 'Route', + funcName: 'constructor', + paramName: 'match' + }); + if (method) { + finalAssertExports.isOneOf(method, validMethods, { + paramName: 'method' + }); + } + } + // These values are referenced directly by Router so cannot be + // altered by minificaton. + this.handler = normalizeHandler(handler); + this.match = match; + this.method = method; + } + /** + * + * @param {workbox-routing-handlerCallback} handler A callback + * function that returns a Promise resolving to a Response + */ + setCatchHandler(handler) { + this.catchHandler = normalizeHandler(handler); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * RegExpRoute makes it easy to create a regular expression based + * {@link workbox-routing.Route}. + * + * For same-origin requests the RegExp only needs to match part of the URL. For + * requests against third-party servers, you must define a RegExp that matches + * the start of the URL. + * + * @memberof workbox-routing + * @extends workbox-routing.Route + */ + class RegExpRoute extends Route { + /** + * If the regular expression contains + * [capture groups]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#grouping-back-references}, + * the captured values will be passed to the + * {@link workbox-routing~handlerCallback} `params` + * argument. + * + * @param {RegExp} regExp The regular expression to match against URLs. + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + * @param {string} [method='GET'] The HTTP method to match the Route + * against. + */ + constructor(regExp, handler, method) { + { + finalAssertExports.isInstance(regExp, RegExp, { + moduleName: 'workbox-routing', + className: 'RegExpRoute', + funcName: 'constructor', + paramName: 'pattern' + }); + } + const match = ({ + url + }) => { + const result = regExp.exec(url.href); + // Return immediately if there's no match. + if (!result) { + return; + } + // Require that the match start at the first character in the URL string + // if it's a cross-origin request. + // See https://github.com/GoogleChrome/workbox/issues/281 for the context + // behind this behavior. + if (url.origin !== location.origin && result.index !== 0) { + { + logger.debug(`The regular expression '${regExp.toString()}' only partially matched ` + `against the cross-origin URL '${url.toString()}'. RegExpRoute's will only ` + `handle cross-origin requests if they match the entire URL.`); + } + return; + } + // If the route matches, but there aren't any capture groups defined, then + // this will return [], which is truthy and therefore sufficient to + // indicate a match. + // If there are capture groups, then it will return their values. + return result.slice(1); + }; + super(match, handler, method); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const getFriendlyURL = url => { + const urlObj = new URL(String(url), location.href); + // See https://github.com/GoogleChrome/workbox/issues/2323 + // We want to include everything, except for the origin if it's same-origin. + return urlObj.href.replace(new RegExp(`^${location.origin}`), ''); + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The Router can be used to process a `FetchEvent` using one or more + * {@link workbox-routing.Route}, responding with a `Response` if + * a matching route exists. + * + * If no route matches a given a request, the Router will use a "default" + * handler if one is defined. + * + * Should the matching Route throw an error, the Router will use a "catch" + * handler if one is defined to gracefully deal with issues and respond with a + * Request. + * + * If a request matches multiple routes, the **earliest** registered route will + * be used to respond to the request. + * + * @memberof workbox-routing + */ + class Router { + /** + * Initializes a new Router. + */ + constructor() { + this._routes = new Map(); + this._defaultHandlerMap = new Map(); + } + /** + * @return {Map>} routes A `Map` of HTTP + * method name ('GET', etc.) to an array of all the corresponding `Route` + * instances that are registered. + */ + get routes() { + return this._routes; + } + /** + * Adds a fetch event listener to respond to events when a route matches + * the event's request. + */ + addFetchListener() { + // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 + self.addEventListener('fetch', event => { + const { + request + } = event; + const responsePromise = this.handleRequest({ + request, + event + }); + if (responsePromise) { + event.respondWith(responsePromise); + } + }); + } + /** + * Adds a message event listener for URLs to cache from the window. + * This is useful to cache resources loaded on the page prior to when the + * service worker started controlling it. + * + * The format of the message data sent from the window should be as follows. + * Where the `urlsToCache` array may consist of URL strings or an array of + * URL string + `requestInit` object (the same as you'd pass to `fetch()`). + * + * ``` + * { + * type: 'CACHE_URLS', + * payload: { + * urlsToCache: [ + * './script1.js', + * './script2.js', + * ['./script3.js', {mode: 'no-cors'}], + * ], + * }, + * } + * ``` + */ + addCacheListener() { + // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 + self.addEventListener('message', event => { + // event.data is type 'any' + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (event.data && event.data.type === 'CACHE_URLS') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { + payload + } = event.data; + { + logger.debug(`Caching URLs from the window`, payload.urlsToCache); + } + const requestPromises = Promise.all(payload.urlsToCache.map(entry => { + if (typeof entry === 'string') { + entry = [entry]; + } + const request = new Request(...entry); + return this.handleRequest({ + request, + event + }); + // TODO(philipwalton): TypeScript errors without this typecast for + // some reason (probably a bug). The real type here should work but + // doesn't: `Array | undefined>`. + })); // TypeScript + event.waitUntil(requestPromises); + // If a MessageChannel was used, reply to the message on success. + if (event.ports && event.ports[0]) { + void requestPromises.then(() => event.ports[0].postMessage(true)); + } + } + }); + } + /** + * Apply the routing rules to a FetchEvent object to get a Response from an + * appropriate Route's handler. + * + * @param {Object} options + * @param {Request} options.request The request to handle. + * @param {ExtendableEvent} options.event The event that triggered the + * request. + * @return {Promise|undefined} A promise is returned if a + * registered route can handle the request. If there is no matching + * route and there's no `defaultHandler`, `undefined` is returned. + */ + handleRequest({ + request, + event + }) { + { + finalAssertExports.isInstance(request, Request, { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'handleRequest', + paramName: 'options.request' + }); + } + const url = new URL(request.url, location.href); + if (!url.protocol.startsWith('http')) { + { + logger.debug(`Workbox Router only supports URLs that start with 'http'.`); + } + return; + } + const sameOrigin = url.origin === location.origin; + const { + params, + route + } = this.findMatchingRoute({ + event, + request, + sameOrigin, + url + }); + let handler = route && route.handler; + const debugMessages = []; + { + if (handler) { + debugMessages.push([`Found a route to handle this request:`, route]); + if (params) { + debugMessages.push([`Passing the following params to the route's handler:`, params]); + } + } + } + // If we don't have a handler because there was no matching route, then + // fall back to defaultHandler if that's defined. + const method = request.method; + if (!handler && this._defaultHandlerMap.has(method)) { + { + debugMessages.push(`Failed to find a matching route. Falling ` + `back to the default handler for ${method}.`); + } + handler = this._defaultHandlerMap.get(method); + } + if (!handler) { + { + // No handler so Workbox will do nothing. If logs is set of debug + // i.e. verbose, we should print out this information. + logger.debug(`No route found for: ${getFriendlyURL(url)}`); + } + return; + } + { + // We have a handler, meaning Workbox is going to handle the route. + // print the routing details to the console. + logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`); + debugMessages.forEach(msg => { + if (Array.isArray(msg)) { + logger.log(...msg); + } else { + logger.log(msg); + } + }); + logger.groupEnd(); + } + // Wrap in try and catch in case the handle method throws a synchronous + // error. It should still callback to the catch handler. + let responsePromise; + try { + responsePromise = handler.handle({ + url, + request, + event, + params + }); + } catch (err) { + responsePromise = Promise.reject(err); + } + // Get route's catch handler, if it exists + const catchHandler = route && route.catchHandler; + if (responsePromise instanceof Promise && (this._catchHandler || catchHandler)) { + responsePromise = responsePromise.catch(async err => { + // If there's a route catch handler, process that first + if (catchHandler) { + { + // Still include URL here as it will be async from the console group + // and may not make sense without the URL + logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to route's Catch Handler.`); + logger.error(`Error thrown by:`, route); + logger.error(err); + logger.groupEnd(); + } + try { + return await catchHandler.handle({ + url, + request, + event, + params + }); + } catch (catchErr) { + if (catchErr instanceof Error) { + err = catchErr; + } + } + } + if (this._catchHandler) { + { + // Still include URL here as it will be async from the console group + // and may not make sense without the URL + logger.groupCollapsed(`Error thrown when responding to: ` + ` ${getFriendlyURL(url)}. Falling back to global Catch Handler.`); + logger.error(`Error thrown by:`, route); + logger.error(err); + logger.groupEnd(); + } + return this._catchHandler.handle({ + url, + request, + event + }); + } + throw err; + }); + } + return responsePromise; + } + /** + * Checks a request and URL (and optionally an event) against the list of + * registered routes, and if there's a match, returns the corresponding + * route along with any params generated by the match. + * + * @param {Object} options + * @param {URL} options.url + * @param {boolean} options.sameOrigin The result of comparing `url.origin` + * against the current origin. + * @param {Request} options.request The request to match. + * @param {Event} options.event The corresponding event. + * @return {Object} An object with `route` and `params` properties. + * They are populated if a matching route was found or `undefined` + * otherwise. + */ + findMatchingRoute({ + url, + sameOrigin, + request, + event + }) { + const routes = this._routes.get(request.method) || []; + for (const route of routes) { + let params; + // route.match returns type any, not possible to change right now. + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const matchResult = route.match({ + url, + sameOrigin, + request, + event + }); + if (matchResult) { + { + // Warn developers that using an async matchCallback is almost always + // not the right thing to do. + if (matchResult instanceof Promise) { + logger.warn(`While routing ${getFriendlyURL(url)}, an async ` + `matchCallback function was used. Please convert the ` + `following route to use a synchronous matchCallback function:`, route); + } + } + // See https://github.com/GoogleChrome/workbox/issues/2079 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + params = matchResult; + if (Array.isArray(params) && params.length === 0) { + // Instead of passing an empty array in as params, use undefined. + params = undefined; + } else if (matchResult.constructor === Object && + // eslint-disable-line + Object.keys(matchResult).length === 0) { + // Instead of passing an empty object in as params, use undefined. + params = undefined; + } else if (typeof matchResult === 'boolean') { + // For the boolean value true (rather than just something truth-y), + // don't set params. + // See https://github.com/GoogleChrome/workbox/pull/2134#issuecomment-513924353 + params = undefined; + } + // Return early if have a match. + return { + route, + params + }; + } + } + // If no match was found above, return and empty object. + return {}; + } + /** + * Define a default `handler` that's called when no routes explicitly + * match the incoming request. + * + * Each HTTP method ('GET', 'POST', etc.) gets its own default handler. + * + * Without a default handler, unmatched requests will go against the + * network as if there were no service worker present. + * + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + * @param {string} [method='GET'] The HTTP method to associate with this + * default handler. Each method has its own default. + */ + setDefaultHandler(handler, method = defaultMethod) { + this._defaultHandlerMap.set(method, normalizeHandler(handler)); + } + /** + * If a Route throws an error while handling a request, this `handler` + * will be called and given a chance to provide a response. + * + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + */ + setCatchHandler(handler) { + this._catchHandler = normalizeHandler(handler); + } + /** + * Registers a route with the router. + * + * @param {workbox-routing.Route} route The route to register. + */ + registerRoute(route) { + { + finalAssertExports.isType(route, 'object', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route' + }); + finalAssertExports.hasMethod(route, 'match', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route' + }); + finalAssertExports.isType(route.handler, 'object', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route' + }); + finalAssertExports.hasMethod(route.handler, 'handle', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route.handler' + }); + finalAssertExports.isType(route.method, 'string', { + moduleName: 'workbox-routing', + className: 'Router', + funcName: 'registerRoute', + paramName: 'route.method' + }); + } + if (!this._routes.has(route.method)) { + this._routes.set(route.method, []); + } + // Give precedence to all of the earlier routes by adding this additional + // route to the end of the array. + this._routes.get(route.method).push(route); + } + /** + * Unregisters a route with the router. + * + * @param {workbox-routing.Route} route The route to unregister. + */ + unregisterRoute(route) { + if (!this._routes.has(route.method)) { + throw new WorkboxError('unregister-route-but-not-found-with-method', { + method: route.method + }); + } + const routeIndex = this._routes.get(route.method).indexOf(route); + if (routeIndex > -1) { + this._routes.get(route.method).splice(routeIndex, 1); + } else { + throw new WorkboxError('unregister-route-route-not-registered'); + } + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + let defaultRouter; + /** + * Creates a new, singleton Router instance if one does not exist. If one + * does already exist, that instance is returned. + * + * @private + * @return {Router} + */ + const getOrCreateDefaultRouter = () => { + if (!defaultRouter) { + defaultRouter = new Router(); + // The helpers that use the default Router assume these listeners exist. + defaultRouter.addFetchListener(); + defaultRouter.addCacheListener(); + } + return defaultRouter; + }; + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Easily register a RegExp, string, or function with a caching + * strategy to a singleton Router instance. + * + * This method will generate a Route for you if needed and + * call {@link workbox-routing.Router#registerRoute}. + * + * @param {RegExp|string|workbox-routing.Route~matchCallback|workbox-routing.Route} capture + * If the capture param is a `Route`, all other arguments will be ignored. + * @param {workbox-routing~handlerCallback} [handler] A callback + * function that returns a Promise resulting in a Response. This parameter + * is required if `capture` is not a `Route` object. + * @param {string} [method='GET'] The HTTP method to match the Route + * against. + * @return {workbox-routing.Route} The generated `Route`. + * + * @memberof workbox-routing + */ + function registerRoute(capture, handler, method) { + let route; + if (typeof capture === 'string') { + const captureUrl = new URL(capture, location.href); + { + if (!(capture.startsWith('/') || capture.startsWith('http'))) { + throw new WorkboxError('invalid-string', { + moduleName: 'workbox-routing', + funcName: 'registerRoute', + paramName: 'capture' + }); + } + // We want to check if Express-style wildcards are in the pathname only. + // TODO: Remove this log message in v4. + const valueToCheck = capture.startsWith('http') ? captureUrl.pathname : capture; + // See https://github.com/pillarjs/path-to-regexp#parameters + const wildcards = '[*:?+]'; + if (new RegExp(`${wildcards}`).exec(valueToCheck)) { + logger.debug(`The '$capture' parameter contains an Express-style wildcard ` + `character (${wildcards}). Strings are now always interpreted as ` + `exact matches; use a RegExp for partial or wildcard matches.`); + } + } + const matchCallback = ({ + url + }) => { + { + if (url.pathname === captureUrl.pathname && url.origin !== captureUrl.origin) { + logger.debug(`${capture} only partially matches the cross-origin URL ` + `${url.toString()}. This route will only handle cross-origin requests ` + `if they match the entire URL.`); + } + } + return url.href === captureUrl.href; + }; + // If `capture` is a string then `handler` and `method` must be present. + route = new Route(matchCallback, handler, method); + } else if (capture instanceof RegExp) { + // If `capture` is a `RegExp` then `handler` and `method` must be present. + route = new RegExpRoute(capture, handler, method); + } else if (typeof capture === 'function') { + // If `capture` is a function then `handler` and `method` must be present. + route = new Route(capture, handler, method); + } else if (capture instanceof Route) { + route = capture; + } else { + throw new WorkboxError('unsupported-route-type', { + moduleName: 'workbox-routing', + funcName: 'registerRoute', + paramName: 'capture' + }); + } + const defaultRouter = getOrCreateDefaultRouter(); + defaultRouter.registerRoute(route); + return route; + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const _cacheNameDetails = { + googleAnalytics: 'googleAnalytics', + precache: 'precache-v2', + prefix: 'workbox', + runtime: 'runtime', + suffix: typeof registration !== 'undefined' ? registration.scope : '' + }; + const _createCacheName = cacheName => { + return [_cacheNameDetails.prefix, cacheName, _cacheNameDetails.suffix].filter(value => value && value.length > 0).join('-'); + }; + const eachCacheNameDetail = fn => { + for (const key of Object.keys(_cacheNameDetails)) { + fn(key); + } + }; + const cacheNames = { + updateDetails: details => { + eachCacheNameDetail(key => { + if (typeof details[key] === 'string') { + _cacheNameDetails[key] = details[key]; + } + }); + }, + getGoogleAnalyticsName: userCacheName => { + return userCacheName || _createCacheName(_cacheNameDetails.googleAnalytics); + }, + getPrecacheName: userCacheName => { + return userCacheName || _createCacheName(_cacheNameDetails.precache); + }, + getPrefix: () => { + return _cacheNameDetails.prefix; + }, + getRuntimeName: userCacheName => { + return userCacheName || _createCacheName(_cacheNameDetails.runtime); + }, + getSuffix: () => { + return _cacheNameDetails.suffix; + } + }; + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A helper function that prevents a promise from being flagged as unused. + * + * @private + **/ + function dontWaitFor(promise) { + // Effective no-op. + void promise.then(() => {}); + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + // Callbacks to be executed whenever there's a quota error. + // Can't change Function type right now. + // eslint-disable-next-line @typescript-eslint/ban-types + const quotaErrorCallbacks = new Set(); + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Adds a function to the set of quotaErrorCallbacks that will be executed if + * there's a quota error. + * + * @param {Function} callback + * @memberof workbox-core + */ + // Can't change Function type + // eslint-disable-next-line @typescript-eslint/ban-types + function registerQuotaErrorCallback(callback) { + { + finalAssertExports.isType(callback, 'function', { + moduleName: 'workbox-core', + funcName: 'register', + paramName: 'callback' + }); + } + quotaErrorCallbacks.add(callback); + { + logger.log('Registered a callback to respond to quota errors.', callback); + } + } + + function _extends() { + return _extends = Object.assign ? Object.assign.bind() : function (n) { + for (var e = 1; e < arguments.length; e++) { + var t = arguments[e]; + for (var r in t) ({}).hasOwnProperty.call(t, r) && (n[r] = t[r]); + } + return n; + }, _extends.apply(null, arguments); + } + + const instanceOfAny = (object, constructors) => constructors.some(c => object instanceof c); + let idbProxyableTypes; + let cursorAdvanceMethods; + // This is a function to prevent it throwing up in node environments. + function getIdbProxyableTypes() { + return idbProxyableTypes || (idbProxyableTypes = [IDBDatabase, IDBObjectStore, IDBIndex, IDBCursor, IDBTransaction]); + } + // This is a function to prevent it throwing up in node environments. + function getCursorAdvanceMethods() { + return cursorAdvanceMethods || (cursorAdvanceMethods = [IDBCursor.prototype.advance, IDBCursor.prototype.continue, IDBCursor.prototype.continuePrimaryKey]); + } + const cursorRequestMap = new WeakMap(); + const transactionDoneMap = new WeakMap(); + const transactionStoreNamesMap = new WeakMap(); + const transformCache = new WeakMap(); + const reverseTransformCache = new WeakMap(); + function promisifyRequest(request) { + const promise = new Promise((resolve, reject) => { + const unlisten = () => { + request.removeEventListener('success', success); + request.removeEventListener('error', error); + }; + const success = () => { + resolve(wrap(request.result)); + unlisten(); + }; + const error = () => { + reject(request.error); + unlisten(); + }; + request.addEventListener('success', success); + request.addEventListener('error', error); + }); + promise.then(value => { + // Since cursoring reuses the IDBRequest (*sigh*), we cache it for later retrieval + // (see wrapFunction). + if (value instanceof IDBCursor) { + cursorRequestMap.set(value, request); + } + // Catching to avoid "Uncaught Promise exceptions" + }).catch(() => {}); + // This mapping exists in reverseTransformCache but doesn't doesn't exist in transformCache. This + // is because we create many promises from a single IDBRequest. + reverseTransformCache.set(promise, request); + return promise; + } + function cacheDonePromiseForTransaction(tx) { + // Early bail if we've already created a done promise for this transaction. + if (transactionDoneMap.has(tx)) return; + const done = new Promise((resolve, reject) => { + const unlisten = () => { + tx.removeEventListener('complete', complete); + tx.removeEventListener('error', error); + tx.removeEventListener('abort', error); + }; + const complete = () => { + resolve(); + unlisten(); + }; + const error = () => { + reject(tx.error || new DOMException('AbortError', 'AbortError')); + unlisten(); + }; + tx.addEventListener('complete', complete); + tx.addEventListener('error', error); + tx.addEventListener('abort', error); + }); + // Cache it for later retrieval. + transactionDoneMap.set(tx, done); + } + let idbProxyTraps = { + get(target, prop, receiver) { + if (target instanceof IDBTransaction) { + // Special handling for transaction.done. + if (prop === 'done') return transactionDoneMap.get(target); + // Polyfill for objectStoreNames because of Edge. + if (prop === 'objectStoreNames') { + return target.objectStoreNames || transactionStoreNamesMap.get(target); + } + // Make tx.store return the only store in the transaction, or undefined if there are many. + if (prop === 'store') { + return receiver.objectStoreNames[1] ? undefined : receiver.objectStore(receiver.objectStoreNames[0]); + } + } + // Else transform whatever we get back. + return wrap(target[prop]); + }, + set(target, prop, value) { + target[prop] = value; + return true; + }, + has(target, prop) { + if (target instanceof IDBTransaction && (prop === 'done' || prop === 'store')) { + return true; + } + return prop in target; + } + }; + function replaceTraps(callback) { + idbProxyTraps = callback(idbProxyTraps); + } + function wrapFunction(func) { + // Due to expected object equality (which is enforced by the caching in `wrap`), we + // only create one new func per func. + // Edge doesn't support objectStoreNames (booo), so we polyfill it here. + if (func === IDBDatabase.prototype.transaction && !('objectStoreNames' in IDBTransaction.prototype)) { + return function (storeNames, ...args) { + const tx = func.call(unwrap(this), storeNames, ...args); + transactionStoreNamesMap.set(tx, storeNames.sort ? storeNames.sort() : [storeNames]); + return wrap(tx); + }; + } + // Cursor methods are special, as the behaviour is a little more different to standard IDB. In + // IDB, you advance the cursor and wait for a new 'success' on the IDBRequest that gave you the + // cursor. It's kinda like a promise that can resolve with many values. That doesn't make sense + // with real promises, so each advance methods returns a new promise for the cursor object, or + // undefined if the end of the cursor has been reached. + if (getCursorAdvanceMethods().includes(func)) { + return function (...args) { + // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use + // the original object. + func.apply(unwrap(this), args); + return wrap(cursorRequestMap.get(this)); + }; + } + return function (...args) { + // Calling the original function with the proxy as 'this' causes ILLEGAL INVOCATION, so we use + // the original object. + return wrap(func.apply(unwrap(this), args)); + }; + } + function transformCachableValue(value) { + if (typeof value === 'function') return wrapFunction(value); + // This doesn't return, it just creates a 'done' promise for the transaction, + // which is later returned for transaction.done (see idbObjectHandler). + if (value instanceof IDBTransaction) cacheDonePromiseForTransaction(value); + if (instanceOfAny(value, getIdbProxyableTypes())) return new Proxy(value, idbProxyTraps); + // Return the same value back if we're not going to transform it. + return value; + } + function wrap(value) { + // We sometimes generate multiple promises from a single IDBRequest (eg when cursoring), because + // IDB is weird and a single IDBRequest can yield many responses, so these can't be cached. + if (value instanceof IDBRequest) return promisifyRequest(value); + // If we've already transformed this value before, reuse the transformed value. + // This is faster, but it also provides object equality. + if (transformCache.has(value)) return transformCache.get(value); + const newValue = transformCachableValue(value); + // Not all types are transformed. + // These may be primitive types, so they can't be WeakMap keys. + if (newValue !== value) { + transformCache.set(value, newValue); + reverseTransformCache.set(newValue, value); + } + return newValue; + } + const unwrap = value => reverseTransformCache.get(value); + + /** + * Open a database. + * + * @param name Name of the database. + * @param version Schema version. + * @param callbacks Additional callbacks. + */ + function openDB(name, version, { + blocked, + upgrade, + blocking, + terminated + } = {}) { + const request = indexedDB.open(name, version); + const openPromise = wrap(request); + if (upgrade) { + request.addEventListener('upgradeneeded', event => { + upgrade(wrap(request.result), event.oldVersion, event.newVersion, wrap(request.transaction), event); + }); + } + if (blocked) { + request.addEventListener('blocked', event => blocked( + // Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405 + event.oldVersion, event.newVersion, event)); + } + openPromise.then(db => { + if (terminated) db.addEventListener('close', () => terminated()); + if (blocking) { + db.addEventListener('versionchange', event => blocking(event.oldVersion, event.newVersion, event)); + } + }).catch(() => {}); + return openPromise; + } + /** + * Delete a database. + * + * @param name Name of the database. + */ + function deleteDB(name, { + blocked + } = {}) { + const request = indexedDB.deleteDatabase(name); + if (blocked) { + request.addEventListener('blocked', event => blocked( + // Casting due to https://github.com/microsoft/TypeScript-DOM-lib-generator/pull/1405 + event.oldVersion, event)); + } + return wrap(request).then(() => undefined); + } + const readMethods = ['get', 'getKey', 'getAll', 'getAllKeys', 'count']; + const writeMethods = ['put', 'add', 'delete', 'clear']; + const cachedMethods = new Map(); + function getMethod(target, prop) { + if (!(target instanceof IDBDatabase && !(prop in target) && typeof prop === 'string')) { + return; + } + if (cachedMethods.get(prop)) return cachedMethods.get(prop); + const targetFuncName = prop.replace(/FromIndex$/, ''); + const useIndex = prop !== targetFuncName; + const isWrite = writeMethods.includes(targetFuncName); + if ( + // Bail if the target doesn't exist on the target. Eg, getAll isn't in Edge. + !(targetFuncName in (useIndex ? IDBIndex : IDBObjectStore).prototype) || !(isWrite || readMethods.includes(targetFuncName))) { + return; + } + const method = async function (storeName, ...args) { + // isWrite ? 'readwrite' : undefined gzipps better, but fails in Edge :( + const tx = this.transaction(storeName, isWrite ? 'readwrite' : 'readonly'); + let target = tx.store; + if (useIndex) target = target.index(args.shift()); + // Must reject if op rejects. + // If it's a write operation, must reject if tx.done rejects. + // Must reject with op rejection first. + // Must resolve with op value. + // Must handle both promises (no unhandled rejections) + return (await Promise.all([target[targetFuncName](...args), isWrite && tx.done]))[0]; + }; + cachedMethods.set(prop, method); + return method; + } + replaceTraps(oldTraps => _extends({}, oldTraps, { + get: (target, prop, receiver) => getMethod(target, prop) || oldTraps.get(target, prop, receiver), + has: (target, prop) => !!getMethod(target, prop) || oldTraps.has(target, prop) + })); + + // @ts-ignore + try { + self['workbox:expiration:7.4.0'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const DB_NAME = 'workbox-expiration'; + const CACHE_OBJECT_STORE = 'cache-entries'; + const normalizeURL = unNormalizedUrl => { + const url = new URL(unNormalizedUrl, location.href); + url.hash = ''; + return url.href; + }; + /** + * Returns the timestamp model. + * + * @private + */ + class CacheTimestampsModel { + /** + * + * @param {string} cacheName + * + * @private + */ + constructor(cacheName) { + this._db = null; + this._cacheName = cacheName; + } + /** + * Performs an upgrade of indexedDB. + * + * @param {IDBPDatabase} db + * + * @private + */ + _upgradeDb(db) { + // TODO(philipwalton): EdgeHTML doesn't support arrays as a keyPath, so we + // have to use the `id` keyPath here and create our own values (a + // concatenation of `url + cacheName`) instead of simply using + // `keyPath: ['url', 'cacheName']`, which is supported in other browsers. + const objStore = db.createObjectStore(CACHE_OBJECT_STORE, { + keyPath: 'id' + }); + // TODO(philipwalton): once we don't have to support EdgeHTML, we can + // create a single index with the keyPath `['cacheName', 'timestamp']` + // instead of doing both these indexes. + objStore.createIndex('cacheName', 'cacheName', { + unique: false + }); + objStore.createIndex('timestamp', 'timestamp', { + unique: false + }); + } + /** + * Performs an upgrade of indexedDB and deletes deprecated DBs. + * + * @param {IDBPDatabase} db + * + * @private + */ + _upgradeDbAndDeleteOldDbs(db) { + this._upgradeDb(db); + if (this._cacheName) { + void deleteDB(this._cacheName); + } + } + /** + * @param {string} url + * @param {number} timestamp + * + * @private + */ + async setTimestamp(url, timestamp) { + url = normalizeURL(url); + const entry = { + url, + timestamp, + cacheName: this._cacheName, + // Creating an ID from the URL and cache name won't be necessary once + // Edge switches to Chromium and all browsers we support work with + // array keyPaths. + id: this._getId(url) + }; + const db = await this.getDb(); + const tx = db.transaction(CACHE_OBJECT_STORE, 'readwrite', { + durability: 'relaxed' + }); + await tx.store.put(entry); + await tx.done; + } + /** + * Returns the timestamp stored for a given URL. + * + * @param {string} url + * @return {number | undefined} + * + * @private + */ + async getTimestamp(url) { + const db = await this.getDb(); + const entry = await db.get(CACHE_OBJECT_STORE, this._getId(url)); + return entry === null || entry === void 0 ? void 0 : entry.timestamp; + } + /** + * Iterates through all the entries in the object store (from newest to + * oldest) and removes entries once either `maxCount` is reached or the + * entry's timestamp is less than `minTimestamp`. + * + * @param {number} minTimestamp + * @param {number} maxCount + * @return {Array} + * + * @private + */ + async expireEntries(minTimestamp, maxCount) { + const db = await this.getDb(); + let cursor = await db.transaction(CACHE_OBJECT_STORE).store.index('timestamp').openCursor(null, 'prev'); + const entriesToDelete = []; + let entriesNotDeletedCount = 0; + while (cursor) { + const result = cursor.value; + // TODO(philipwalton): once we can use a multi-key index, we + // won't have to check `cacheName` here. + if (result.cacheName === this._cacheName) { + // Delete an entry if it's older than the max age or + // if we already have the max number allowed. + if (minTimestamp && result.timestamp < minTimestamp || maxCount && entriesNotDeletedCount >= maxCount) { + // TODO(philipwalton): we should be able to delete the + // entry right here, but doing so causes an iteration + // bug in Safari stable (fixed in TP). Instead we can + // store the keys of the entries to delete, and then + // delete the separate transactions. + // https://github.com/GoogleChrome/workbox/issues/1978 + // cursor.delete(); + // We only need to return the URL, not the whole entry. + entriesToDelete.push(cursor.value); + } else { + entriesNotDeletedCount++; + } + } + cursor = await cursor.continue(); + } + // TODO(philipwalton): once the Safari bug in the following issue is fixed, + // we should be able to remove this loop and do the entry deletion in the + // cursor loop above: + // https://github.com/GoogleChrome/workbox/issues/1978 + const urlsDeleted = []; + for (const entry of entriesToDelete) { + await db.delete(CACHE_OBJECT_STORE, entry.id); + urlsDeleted.push(entry.url); + } + return urlsDeleted; + } + /** + * Takes a URL and returns an ID that will be unique in the object store. + * + * @param {string} url + * @return {string} + * + * @private + */ + _getId(url) { + // Creating an ID from the URL and cache name won't be necessary once + // Edge switches to Chromium and all browsers we support work with + // array keyPaths. + return this._cacheName + '|' + normalizeURL(url); + } + /** + * Returns an open connection to the database. + * + * @private + */ + async getDb() { + if (!this._db) { + this._db = await openDB(DB_NAME, 1, { + upgrade: this._upgradeDbAndDeleteOldDbs.bind(this) + }); + } + return this._db; + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The `CacheExpiration` class allows you define an expiration and / or + * limit on the number of responses stored in a + * [`Cache`](https://developer.mozilla.org/en-US/docs/Web/API/Cache). + * + * @memberof workbox-expiration + */ + class CacheExpiration { + /** + * To construct a new CacheExpiration instance you must provide at least + * one of the `config` properties. + * + * @param {string} cacheName Name of the cache to apply restrictions to. + * @param {Object} config + * @param {number} [config.maxEntries] The maximum number of entries to cache. + * Entries used the least will be removed as the maximum is reached. + * @param {number} [config.maxAgeSeconds] The maximum age of an entry before + * it's treated as stale and removed. + * @param {Object} [config.matchOptions] The [`CacheQueryOptions`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/delete#Parameters) + * that will be used when calling `delete()` on the cache. + */ + constructor(cacheName, config = {}) { + this._isRunning = false; + this._rerunRequested = false; + { + finalAssertExports.isType(cacheName, 'string', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'constructor', + paramName: 'cacheName' + }); + if (!(config.maxEntries || config.maxAgeSeconds)) { + throw new WorkboxError('max-entries-or-age-required', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'constructor' + }); + } + if (config.maxEntries) { + finalAssertExports.isType(config.maxEntries, 'number', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'constructor', + paramName: 'config.maxEntries' + }); + } + if (config.maxAgeSeconds) { + finalAssertExports.isType(config.maxAgeSeconds, 'number', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'constructor', + paramName: 'config.maxAgeSeconds' + }); + } + } + this._maxEntries = config.maxEntries; + this._maxAgeSeconds = config.maxAgeSeconds; + this._matchOptions = config.matchOptions; + this._cacheName = cacheName; + this._timestampModel = new CacheTimestampsModel(cacheName); + } + /** + * Expires entries for the given cache and given criteria. + */ + async expireEntries() { + if (this._isRunning) { + this._rerunRequested = true; + return; + } + this._isRunning = true; + const minTimestamp = this._maxAgeSeconds ? Date.now() - this._maxAgeSeconds * 1000 : 0; + const urlsExpired = await this._timestampModel.expireEntries(minTimestamp, this._maxEntries); + // Delete URLs from the cache + const cache = await self.caches.open(this._cacheName); + for (const url of urlsExpired) { + await cache.delete(url, this._matchOptions); + } + { + if (urlsExpired.length > 0) { + logger.groupCollapsed(`Expired ${urlsExpired.length} ` + `${urlsExpired.length === 1 ? 'entry' : 'entries'} and removed ` + `${urlsExpired.length === 1 ? 'it' : 'them'} from the ` + `'${this._cacheName}' cache.`); + logger.log(`Expired the following ${urlsExpired.length === 1 ? 'URL' : 'URLs'}:`); + urlsExpired.forEach(url => logger.log(` ${url}`)); + logger.groupEnd(); + } else { + logger.debug(`Cache expiration ran and found no entries to remove.`); + } + } + this._isRunning = false; + if (this._rerunRequested) { + this._rerunRequested = false; + dontWaitFor(this.expireEntries()); + } + } + /** + * Update the timestamp for the given URL. This ensures the when + * removing entries based on maximum entries, most recently used + * is accurate or when expiring, the timestamp is up-to-date. + * + * @param {string} url + */ + async updateTimestamp(url) { + { + finalAssertExports.isType(url, 'string', { + moduleName: 'workbox-expiration', + className: 'CacheExpiration', + funcName: 'updateTimestamp', + paramName: 'url' + }); + } + await this._timestampModel.setTimestamp(url, Date.now()); + } + /** + * Can be used to check if a URL has expired or not before it's used. + * + * This requires a look up from IndexedDB, so can be slow. + * + * Note: This method will not remove the cached entry, call + * `expireEntries()` to remove indexedDB and Cache entries. + * + * @param {string} url + * @return {boolean} + */ + async isURLExpired(url) { + if (!this._maxAgeSeconds) { + { + throw new WorkboxError(`expired-test-without-max-age`, { + methodName: 'isURLExpired', + paramName: 'maxAgeSeconds' + }); + } + } else { + const timestamp = await this._timestampModel.getTimestamp(url); + const expireOlderThan = Date.now() - this._maxAgeSeconds * 1000; + return timestamp !== undefined ? timestamp < expireOlderThan : true; + } + } + /** + * Removes the IndexedDB object store used to keep track of cache expiration + * metadata. + */ + async delete() { + // Make sure we don't attempt another rerun if we're called in the middle of + // a cache expiration. + this._rerunRequested = false; + await this._timestampModel.expireEntries(Infinity); // Expires all. + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This plugin can be used in a `workbox-strategy` to regularly enforce a + * limit on the age and / or the number of cached requests. + * + * It can only be used with `workbox-strategy` instances that have a + * [custom `cacheName` property set](/web/tools/workbox/guides/configure-workbox#custom_cache_names_in_strategies). + * In other words, it can't be used to expire entries in strategy that uses the + * default runtime cache name. + * + * Whenever a cached response is used or updated, this plugin will look + * at the associated cache and remove any old or extra responses. + * + * When using `maxAgeSeconds`, responses may be used *once* after expiring + * because the expiration clean up will not have occurred until *after* the + * cached response has been used. If the response has a "Date" header, then + * a light weight expiration check is performed and the response will not be + * used immediately. + * + * When using `maxEntries`, the entry least-recently requested will be removed + * from the cache first. + * + * @memberof workbox-expiration + */ + class ExpirationPlugin { + /** + * @param {ExpirationPluginOptions} config + * @param {number} [config.maxEntries] The maximum number of entries to cache. + * Entries used the least will be removed as the maximum is reached. + * @param {number} [config.maxAgeSeconds] The maximum age of an entry before + * it's treated as stale and removed. + * @param {Object} [config.matchOptions] The [`CacheQueryOptions`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/delete#Parameters) + * that will be used when calling `delete()` on the cache. + * @param {boolean} [config.purgeOnQuotaError] Whether to opt this cache in to + * automatic deletion if the available storage quota has been exceeded. + */ + constructor(config = {}) { + /** + * A "lifecycle" callback that will be triggered automatically by the + * `workbox-strategies` handlers when a `Response` is about to be returned + * from a [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache) to + * the handler. It allows the `Response` to be inspected for freshness and + * prevents it from being used if the `Response`'s `Date` header value is + * older than the configured `maxAgeSeconds`. + * + * @param {Object} options + * @param {string} options.cacheName Name of the cache the response is in. + * @param {Response} options.cachedResponse The `Response` object that's been + * read from a cache and whose freshness should be checked. + * @return {Response} Either the `cachedResponse`, if it's + * fresh, or `null` if the `Response` is older than `maxAgeSeconds`. + * + * @private + */ + this.cachedResponseWillBeUsed = async ({ + event, + request, + cacheName, + cachedResponse + }) => { + if (!cachedResponse) { + return null; + } + const isFresh = this._isResponseDateFresh(cachedResponse); + // Expire entries to ensure that even if the expiration date has + // expired, it'll only be used once. + const cacheExpiration = this._getCacheExpiration(cacheName); + dontWaitFor(cacheExpiration.expireEntries()); + // Update the metadata for the request URL to the current timestamp, + // but don't `await` it as we don't want to block the response. + const updateTimestampDone = cacheExpiration.updateTimestamp(request.url); + if (event) { + try { + event.waitUntil(updateTimestampDone); + } catch (error) { + { + // The event may not be a fetch event; only log the URL if it is. + if ('request' in event) { + logger.warn(`Unable to ensure service worker stays alive when ` + `updating cache entry for ` + `'${getFriendlyURL(event.request.url)}'.`); + } + } + } + } + return isFresh ? cachedResponse : null; + }; + /** + * A "lifecycle" callback that will be triggered automatically by the + * `workbox-strategies` handlers when an entry is added to a cache. + * + * @param {Object} options + * @param {string} options.cacheName Name of the cache that was updated. + * @param {string} options.request The Request for the cached entry. + * + * @private + */ + this.cacheDidUpdate = async ({ + cacheName, + request + }) => { + { + finalAssertExports.isType(cacheName, 'string', { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'cacheDidUpdate', + paramName: 'cacheName' + }); + finalAssertExports.isInstance(request, Request, { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'cacheDidUpdate', + paramName: 'request' + }); + } + const cacheExpiration = this._getCacheExpiration(cacheName); + await cacheExpiration.updateTimestamp(request.url); + await cacheExpiration.expireEntries(); + }; + { + if (!(config.maxEntries || config.maxAgeSeconds)) { + throw new WorkboxError('max-entries-or-age-required', { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'constructor' + }); + } + if (config.maxEntries) { + finalAssertExports.isType(config.maxEntries, 'number', { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'constructor', + paramName: 'config.maxEntries' + }); + } + if (config.maxAgeSeconds) { + finalAssertExports.isType(config.maxAgeSeconds, 'number', { + moduleName: 'workbox-expiration', + className: 'Plugin', + funcName: 'constructor', + paramName: 'config.maxAgeSeconds' + }); + } + } + this._config = config; + this._maxAgeSeconds = config.maxAgeSeconds; + this._cacheExpirations = new Map(); + if (config.purgeOnQuotaError) { + registerQuotaErrorCallback(() => this.deleteCacheAndMetadata()); + } + } + /** + * A simple helper method to return a CacheExpiration instance for a given + * cache name. + * + * @param {string} cacheName + * @return {CacheExpiration} + * + * @private + */ + _getCacheExpiration(cacheName) { + if (cacheName === cacheNames.getRuntimeName()) { + throw new WorkboxError('expire-custom-caches-only'); + } + let cacheExpiration = this._cacheExpirations.get(cacheName); + if (!cacheExpiration) { + cacheExpiration = new CacheExpiration(cacheName, this._config); + this._cacheExpirations.set(cacheName, cacheExpiration); + } + return cacheExpiration; + } + /** + * @param {Response} cachedResponse + * @return {boolean} + * + * @private + */ + _isResponseDateFresh(cachedResponse) { + if (!this._maxAgeSeconds) { + // We aren't expiring by age, so return true, it's fresh + return true; + } + // Check if the 'date' header will suffice a quick expiration check. + // See https://github.com/GoogleChromeLabs/sw-toolbox/issues/164 for + // discussion. + const dateHeaderTimestamp = this._getDateHeaderTimestamp(cachedResponse); + if (dateHeaderTimestamp === null) { + // Unable to parse date, so assume it's fresh. + return true; + } + // If we have a valid headerTime, then our response is fresh iff the + // headerTime plus maxAgeSeconds is greater than the current time. + const now = Date.now(); + return dateHeaderTimestamp >= now - this._maxAgeSeconds * 1000; + } + /** + * This method will extract the data header and parse it into a useful + * value. + * + * @param {Response} cachedResponse + * @return {number|null} + * + * @private + */ + _getDateHeaderTimestamp(cachedResponse) { + if (!cachedResponse.headers.has('date')) { + return null; + } + const dateHeader = cachedResponse.headers.get('date'); + const parsedDate = new Date(dateHeader); + const headerTime = parsedDate.getTime(); + // If the Date header was invalid for some reason, parsedDate.getTime() + // will return NaN. + if (isNaN(headerTime)) { + return null; + } + return headerTime; + } + /** + * This is a helper method that performs two operations: + * + * - Deletes *all* the underlying Cache instances associated with this plugin + * instance, by calling caches.delete() on your behalf. + * - Deletes the metadata from IndexedDB used to keep track of expiration + * details for each Cache instance. + * + * When using cache expiration, calling this method is preferable to calling + * `caches.delete()` directly, since this will ensure that the IndexedDB + * metadata is also cleanly removed and open IndexedDB instances are deleted. + * + * Note that if you're *not* using cache expiration for a given cache, calling + * `caches.delete()` and passing in the cache's name should be sufficient. + * There is no Workbox-specific method needed for cleanup in that case. + */ + async deleteCacheAndMetadata() { + // Do this one at a time instead of all at once via `Promise.all()` to + // reduce the chance of inconsistency if a promise rejects. + for (const [cacheName, cacheExpiration] of this._cacheExpirations) { + await self.caches.delete(cacheName); + await cacheExpiration.delete(); + } + // Reset this._cacheExpirations to its initial state. + this._cacheExpirations = new Map(); + } + } + + // @ts-ignore + try { + self['workbox:cacheable-response:7.4.0'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This class allows you to set up rules determining what + * status codes and/or headers need to be present in order for a + * [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) + * to be considered cacheable. + * + * @memberof workbox-cacheable-response + */ + class CacheableResponse { + /** + * To construct a new CacheableResponse instance you must provide at least + * one of the `config` properties. + * + * If both `statuses` and `headers` are specified, then both conditions must + * be met for the `Response` to be considered cacheable. + * + * @param {Object} config + * @param {Array} [config.statuses] One or more status codes that a + * `Response` can have and be considered cacheable. + * @param {Object} [config.headers] A mapping of header names + * and expected values that a `Response` can have and be considered cacheable. + * If multiple headers are provided, only one needs to be present. + */ + constructor(config = {}) { + { + if (!(config.statuses || config.headers)) { + throw new WorkboxError('statuses-or-headers-required', { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'constructor' + }); + } + if (config.statuses) { + finalAssertExports.isArray(config.statuses, { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'constructor', + paramName: 'config.statuses' + }); + } + if (config.headers) { + finalAssertExports.isType(config.headers, 'object', { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'constructor', + paramName: 'config.headers' + }); + } + } + this._statuses = config.statuses; + this._headers = config.headers; + } + /** + * Checks a response to see whether it's cacheable or not, based on this + * object's configuration. + * + * @param {Response} response The response whose cacheability is being + * checked. + * @return {boolean} `true` if the `Response` is cacheable, and `false` + * otherwise. + */ + isResponseCacheable(response) { + { + finalAssertExports.isInstance(response, Response, { + moduleName: 'workbox-cacheable-response', + className: 'CacheableResponse', + funcName: 'isResponseCacheable', + paramName: 'response' + }); + } + let cacheable = true; + if (this._statuses) { + cacheable = this._statuses.includes(response.status); + } + if (this._headers && cacheable) { + cacheable = Object.keys(this._headers).some(headerName => { + return response.headers.get(headerName) === this._headers[headerName]; + }); + } + { + if (!cacheable) { + logger.groupCollapsed(`The request for ` + `'${getFriendlyURL(response.url)}' returned a response that does ` + `not meet the criteria for being cached.`); + logger.groupCollapsed(`View cacheability criteria here.`); + logger.log(`Cacheable statuses: ` + JSON.stringify(this._statuses)); + logger.log(`Cacheable headers: ` + JSON.stringify(this._headers, null, 2)); + logger.groupEnd(); + const logFriendlyHeaders = {}; + response.headers.forEach((value, key) => { + logFriendlyHeaders[key] = value; + }); + logger.groupCollapsed(`View response status and headers here.`); + logger.log(`Response status: ${response.status}`); + logger.log(`Response headers: ` + JSON.stringify(logFriendlyHeaders, null, 2)); + logger.groupEnd(); + logger.groupCollapsed(`View full response details here.`); + logger.log(response.headers); + logger.log(response); + logger.groupEnd(); + logger.groupEnd(); + } + } + return cacheable; + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A class implementing the `cacheWillUpdate` lifecycle callback. This makes it + * easier to add in cacheability checks to requests made via Workbox's built-in + * strategies. + * + * @memberof workbox-cacheable-response + */ + class CacheableResponsePlugin { + /** + * To construct a new CacheableResponsePlugin instance you must provide at + * least one of the `config` properties. + * + * If both `statuses` and `headers` are specified, then both conditions must + * be met for the `Response` to be considered cacheable. + * + * @param {Object} config + * @param {Array} [config.statuses] One or more status codes that a + * `Response` can have and be considered cacheable. + * @param {Object} [config.headers] A mapping of header names + * and expected values that a `Response` can have and be considered cacheable. + * If multiple headers are provided, only one needs to be present. + */ + constructor(config) { + /** + * @param {Object} options + * @param {Response} options.response + * @return {Response|null} + * @private + */ + this.cacheWillUpdate = async ({ + response + }) => { + if (this._cacheableResponse.isResponseCacheable(response)) { + return response; + } + return null; + }; + this._cacheableResponse = new CacheableResponse(config); + } + } + + // @ts-ignore + try { + self['workbox:strategies:7.4.0'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const cacheOkAndOpaquePlugin = { + /** + * Returns a valid response (to allow caching) if the status is 200 (OK) or + * 0 (opaque). + * + * @param {Object} options + * @param {Response} options.response + * @return {Response|null} + * + * @private + */ + cacheWillUpdate: async ({ + response + }) => { + if (response.status === 200 || response.status === 0) { + return response; + } + return null; + } + }; + + /* + Copyright 2020 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + function stripParams(fullURL, ignoreParams) { + const strippedURL = new URL(fullURL); + for (const param of ignoreParams) { + strippedURL.searchParams.delete(param); + } + return strippedURL.href; + } + /** + * Matches an item in the cache, ignoring specific URL params. This is similar + * to the `ignoreSearch` option, but it allows you to ignore just specific + * params (while continuing to match on the others). + * + * @private + * @param {Cache} cache + * @param {Request} request + * @param {Object} matchOptions + * @param {Array} ignoreParams + * @return {Promise} + */ + async function cacheMatchIgnoreParams(cache, request, ignoreParams, matchOptions) { + const strippedRequestURL = stripParams(request.url, ignoreParams); + // If the request doesn't include any ignored params, match as normal. + if (request.url === strippedRequestURL) { + return cache.match(request, matchOptions); + } + // Otherwise, match by comparing keys + const keysOptions = Object.assign(Object.assign({}, matchOptions), { + ignoreSearch: true + }); + const cacheKeys = await cache.keys(request, keysOptions); + for (const cacheKey of cacheKeys) { + const strippedCacheKeyURL = stripParams(cacheKey.url, ignoreParams); + if (strippedRequestURL === strippedCacheKeyURL) { + return cache.match(cacheKey, matchOptions); + } + } + return; + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * The Deferred class composes Promises in a way that allows for them to be + * resolved or rejected from outside the constructor. In most cases promises + * should be used directly, but Deferreds can be necessary when the logic to + * resolve a promise must be separate. + * + * @private + */ + class Deferred { + /** + * Creates a promise and exposes its resolve and reject functions as methods. + */ + constructor() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Runs all of the callback functions, one at a time sequentially, in the order + * in which they were registered. + * + * @memberof workbox-core + * @private + */ + async function executeQuotaErrorCallbacks() { + { + logger.log(`About to run ${quotaErrorCallbacks.size} ` + `callbacks to clean up caches.`); + } + for (const callback of quotaErrorCallbacks) { + await callback(); + { + logger.log(callback, 'is complete.'); + } + } + { + logger.log('Finished running callbacks.'); + } + } + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Returns a promise that resolves and the passed number of milliseconds. + * This utility is an async/await-friendly version of `setTimeout`. + * + * @param {number} ms + * @return {Promise} + * @private + */ + function timeout(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + } + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + function toRequest(input) { + return typeof input === 'string' ? new Request(input) : input; + } + /** + * A class created every time a Strategy instance calls + * {@link workbox-strategies.Strategy~handle} or + * {@link workbox-strategies.Strategy~handleAll} that wraps all fetch and + * cache actions around plugin callbacks and keeps track of when the strategy + * is "done" (i.e. all added `event.waitUntil()` promises have resolved). + * + * @memberof workbox-strategies + */ + class StrategyHandler { + /** + * Creates a new instance associated with the passed strategy and event + * that's handling the request. + * + * The constructor also initializes the state that will be passed to each of + * the plugins handling this request. + * + * @param {workbox-strategies.Strategy} strategy + * @param {Object} options + * @param {Request|string} options.request A request to run this strategy for. + * @param {ExtendableEvent} options.event The event associated with the + * request. + * @param {URL} [options.url] + * @param {*} [options.params] The return value from the + * {@link workbox-routing~matchCallback} (if applicable). + */ + constructor(strategy, options) { + this._cacheKeys = {}; + /** + * The request the strategy is performing (passed to the strategy's + * `handle()` or `handleAll()` method). + * @name request + * @instance + * @type {Request} + * @memberof workbox-strategies.StrategyHandler + */ + /** + * The event associated with this request. + * @name event + * @instance + * @type {ExtendableEvent} + * @memberof workbox-strategies.StrategyHandler + */ + /** + * A `URL` instance of `request.url` (if passed to the strategy's + * `handle()` or `handleAll()` method). + * Note: the `url` param will be present if the strategy was invoked + * from a workbox `Route` object. + * @name url + * @instance + * @type {URL|undefined} + * @memberof workbox-strategies.StrategyHandler + */ + /** + * A `param` value (if passed to the strategy's + * `handle()` or `handleAll()` method). + * Note: the `param` param will be present if the strategy was invoked + * from a workbox `Route` object and the + * {@link workbox-routing~matchCallback} returned + * a truthy value (it will be that value). + * @name params + * @instance + * @type {*|undefined} + * @memberof workbox-strategies.StrategyHandler + */ + { + finalAssertExports.isInstance(options.event, ExtendableEvent, { + moduleName: 'workbox-strategies', + className: 'StrategyHandler', + funcName: 'constructor', + paramName: 'options.event' + }); + } + Object.assign(this, options); + this.event = options.event; + this._strategy = strategy; + this._handlerDeferred = new Deferred(); + this._extendLifetimePromises = []; + // Copy the plugins list (since it's mutable on the strategy), + // so any mutations don't affect this handler instance. + this._plugins = [...strategy.plugins]; + this._pluginStateMap = new Map(); + for (const plugin of this._plugins) { + this._pluginStateMap.set(plugin, {}); + } + this.event.waitUntil(this._handlerDeferred.promise); + } + /** + * Fetches a given request (and invokes any applicable plugin callback + * methods) using the `fetchOptions` (for non-navigation requests) and + * `plugins` defined on the `Strategy` object. + * + * The following plugin lifecycle methods are invoked when using this method: + * - `requestWillFetch()` + * - `fetchDidSucceed()` + * - `fetchDidFail()` + * + * @param {Request|string} input The URL or request to fetch. + * @return {Promise} + */ + async fetch(input) { + const { + event + } = this; + let request = toRequest(input); + if (request.mode === 'navigate' && event instanceof FetchEvent && event.preloadResponse) { + const possiblePreloadResponse = await event.preloadResponse; + if (possiblePreloadResponse) { + { + logger.log(`Using a preloaded navigation response for ` + `'${getFriendlyURL(request.url)}'`); + } + return possiblePreloadResponse; + } + } + // If there is a fetchDidFail plugin, we need to save a clone of the + // original request before it's either modified by a requestWillFetch + // plugin or before the original request's body is consumed via fetch(). + const originalRequest = this.hasCallback('fetchDidFail') ? request.clone() : null; + try { + for (const cb of this.iterateCallbacks('requestWillFetch')) { + request = await cb({ + request: request.clone(), + event + }); + } + } catch (err) { + if (err instanceof Error) { + throw new WorkboxError('plugin-error-request-will-fetch', { + thrownErrorMessage: err.message + }); + } + } + // The request can be altered by plugins with `requestWillFetch` making + // the original request (most likely from a `fetch` event) different + // from the Request we make. Pass both to `fetchDidFail` to aid debugging. + const pluginFilteredRequest = request.clone(); + try { + let fetchResponse; + // See https://github.com/GoogleChrome/workbox/issues/1796 + fetchResponse = await fetch(request, request.mode === 'navigate' ? undefined : this._strategy.fetchOptions); + if ("development" !== 'production') { + logger.debug(`Network request for ` + `'${getFriendlyURL(request.url)}' returned a response with ` + `status '${fetchResponse.status}'.`); + } + for (const callback of this.iterateCallbacks('fetchDidSucceed')) { + fetchResponse = await callback({ + event, + request: pluginFilteredRequest, + response: fetchResponse + }); + } + return fetchResponse; + } catch (error) { + { + logger.log(`Network request for ` + `'${getFriendlyURL(request.url)}' threw an error.`, error); + } + // `originalRequest` will only exist if a `fetchDidFail` callback + // is being used (see above). + if (originalRequest) { + await this.runCallbacks('fetchDidFail', { + error: error, + event, + originalRequest: originalRequest.clone(), + request: pluginFilteredRequest.clone() + }); + } + throw error; + } + } + /** + * Calls `this.fetch()` and (in the background) runs `this.cachePut()` on + * the response generated by `this.fetch()`. + * + * The call to `this.cachePut()` automatically invokes `this.waitUntil()`, + * so you do not have to manually call `waitUntil()` on the event. + * + * @param {Request|string} input The request or URL to fetch and cache. + * @return {Promise} + */ + async fetchAndCachePut(input) { + const response = await this.fetch(input); + const responseClone = response.clone(); + void this.waitUntil(this.cachePut(input, responseClone)); + return response; + } + /** + * Matches a request from the cache (and invokes any applicable plugin + * callback methods) using the `cacheName`, `matchOptions`, and `plugins` + * defined on the strategy object. + * + * The following plugin lifecycle methods are invoked when using this method: + * - cacheKeyWillBeUsed() + * - cachedResponseWillBeUsed() + * + * @param {Request|string} key The Request or URL to use as the cache key. + * @return {Promise} A matching response, if found. + */ + async cacheMatch(key) { + const request = toRequest(key); + let cachedResponse; + const { + cacheName, + matchOptions + } = this._strategy; + const effectiveRequest = await this.getCacheKey(request, 'read'); + const multiMatchOptions = Object.assign(Object.assign({}, matchOptions), { + cacheName + }); + cachedResponse = await caches.match(effectiveRequest, multiMatchOptions); + { + if (cachedResponse) { + logger.debug(`Found a cached response in '${cacheName}'.`); + } else { + logger.debug(`No cached response found in '${cacheName}'.`); + } + } + for (const callback of this.iterateCallbacks('cachedResponseWillBeUsed')) { + cachedResponse = (await callback({ + cacheName, + matchOptions, + cachedResponse, + request: effectiveRequest, + event: this.event + })) || undefined; + } + return cachedResponse; + } + /** + * Puts a request/response pair in the cache (and invokes any applicable + * plugin callback methods) using the `cacheName` and `plugins` defined on + * the strategy object. + * + * The following plugin lifecycle methods are invoked when using this method: + * - cacheKeyWillBeUsed() + * - cacheWillUpdate() + * - cacheDidUpdate() + * + * @param {Request|string} key The request or URL to use as the cache key. + * @param {Response} response The response to cache. + * @return {Promise} `false` if a cacheWillUpdate caused the response + * not be cached, and `true` otherwise. + */ + async cachePut(key, response) { + const request = toRequest(key); + // Run in the next task to avoid blocking other cache reads. + // https://github.com/w3c/ServiceWorker/issues/1397 + await timeout(0); + const effectiveRequest = await this.getCacheKey(request, 'write'); + { + if (effectiveRequest.method && effectiveRequest.method !== 'GET') { + throw new WorkboxError('attempt-to-cache-non-get-request', { + url: getFriendlyURL(effectiveRequest.url), + method: effectiveRequest.method + }); + } + // See https://github.com/GoogleChrome/workbox/issues/2818 + const vary = response.headers.get('Vary'); + if (vary) { + logger.debug(`The response for ${getFriendlyURL(effectiveRequest.url)} ` + `has a 'Vary: ${vary}' header. ` + `Consider setting the {ignoreVary: true} option on your strategy ` + `to ensure cache matching and deletion works as expected.`); + } + } + if (!response) { + { + logger.error(`Cannot cache non-existent response for ` + `'${getFriendlyURL(effectiveRequest.url)}'.`); + } + throw new WorkboxError('cache-put-with-no-response', { + url: getFriendlyURL(effectiveRequest.url) + }); + } + const responseToCache = await this._ensureResponseSafeToCache(response); + if (!responseToCache) { + { + logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' ` + `will not be cached.`, responseToCache); + } + return false; + } + const { + cacheName, + matchOptions + } = this._strategy; + const cache = await self.caches.open(cacheName); + const hasCacheUpdateCallback = this.hasCallback('cacheDidUpdate'); + const oldResponse = hasCacheUpdateCallback ? await cacheMatchIgnoreParams( + // TODO(philipwalton): the `__WB_REVISION__` param is a precaching + // feature. Consider into ways to only add this behavior if using + // precaching. + cache, effectiveRequest.clone(), ['__WB_REVISION__'], matchOptions) : null; + { + logger.debug(`Updating the '${cacheName}' cache with a new Response ` + `for ${getFriendlyURL(effectiveRequest.url)}.`); + } + try { + await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache); + } catch (error) { + if (error instanceof Error) { + // See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError + if (error.name === 'QuotaExceededError') { + await executeQuotaErrorCallbacks(); + } + throw error; + } + } + for (const callback of this.iterateCallbacks('cacheDidUpdate')) { + await callback({ + cacheName, + oldResponse, + newResponse: responseToCache.clone(), + request: effectiveRequest, + event: this.event + }); + } + return true; + } + /** + * Checks the list of plugins for the `cacheKeyWillBeUsed` callback, and + * executes any of those callbacks found in sequence. The final `Request` + * object returned by the last plugin is treated as the cache key for cache + * reads and/or writes. If no `cacheKeyWillBeUsed` plugin callbacks have + * been registered, the passed request is returned unmodified + * + * @param {Request} request + * @param {string} mode + * @return {Promise} + */ + async getCacheKey(request, mode) { + const key = `${request.url} | ${mode}`; + if (!this._cacheKeys[key]) { + let effectiveRequest = request; + for (const callback of this.iterateCallbacks('cacheKeyWillBeUsed')) { + effectiveRequest = toRequest(await callback({ + mode, + request: effectiveRequest, + event: this.event, + // params has a type any can't change right now. + params: this.params // eslint-disable-line + })); + } + this._cacheKeys[key] = effectiveRequest; + } + return this._cacheKeys[key]; + } + /** + * Returns true if the strategy has at least one plugin with the given + * callback. + * + * @param {string} name The name of the callback to check for. + * @return {boolean} + */ + hasCallback(name) { + for (const plugin of this._strategy.plugins) { + if (name in plugin) { + return true; + } + } + return false; + } + /** + * Runs all plugin callbacks matching the given name, in order, passing the + * given param object (merged ith the current plugin state) as the only + * argument. + * + * Note: since this method runs all plugins, it's not suitable for cases + * where the return value of a callback needs to be applied prior to calling + * the next callback. See + * {@link workbox-strategies.StrategyHandler#iterateCallbacks} + * below for how to handle that case. + * + * @param {string} name The name of the callback to run within each plugin. + * @param {Object} param The object to pass as the first (and only) param + * when executing each callback. This object will be merged with the + * current plugin state prior to callback execution. + */ + async runCallbacks(name, param) { + for (const callback of this.iterateCallbacks(name)) { + // TODO(philipwalton): not sure why `any` is needed. It seems like + // this should work with `as WorkboxPluginCallbackParam[C]`. + await callback(param); + } + } + /** + * Accepts a callback and returns an iterable of matching plugin callbacks, + * where each callback is wrapped with the current handler state (i.e. when + * you call each callback, whatever object parameter you pass it will + * be merged with the plugin's current state). + * + * @param {string} name The name fo the callback to run + * @return {Array} + */ + *iterateCallbacks(name) { + for (const plugin of this._strategy.plugins) { + if (typeof plugin[name] === 'function') { + const state = this._pluginStateMap.get(plugin); + const statefulCallback = param => { + const statefulParam = Object.assign(Object.assign({}, param), { + state + }); + // TODO(philipwalton): not sure why `any` is needed. It seems like + // this should work with `as WorkboxPluginCallbackParam[C]`. + return plugin[name](statefulParam); + }; + yield statefulCallback; + } + } + } + /** + * Adds a promise to the + * [extend lifetime promises]{@link https://w3c.github.io/ServiceWorker/#extendableevent-extend-lifetime-promises} + * of the event associated with the request being handled (usually a + * `FetchEvent`). + * + * Note: you can await + * {@link workbox-strategies.StrategyHandler~doneWaiting} + * to know when all added promises have settled. + * + * @param {Promise} promise A promise to add to the extend lifetime promises + * of the event that triggered the request. + */ + waitUntil(promise) { + this._extendLifetimePromises.push(promise); + return promise; + } + /** + * Returns a promise that resolves once all promises passed to + * {@link workbox-strategies.StrategyHandler~waitUntil} + * have settled. + * + * Note: any work done after `doneWaiting()` settles should be manually + * passed to an event's `waitUntil()` method (not this handler's + * `waitUntil()` method), otherwise the service worker thread may be killed + * prior to your work completing. + */ + async doneWaiting() { + while (this._extendLifetimePromises.length) { + const promises = this._extendLifetimePromises.splice(0); + const result = await Promise.allSettled(promises); + const firstRejection = result.find(i => i.status === 'rejected'); + if (firstRejection) { + throw firstRejection.reason; + } + } + } + /** + * Stops running the strategy and immediately resolves any pending + * `waitUntil()` promises. + */ + destroy() { + this._handlerDeferred.resolve(null); + } + /** + * This method will call cacheWillUpdate on the available plugins (or use + * status === 200) to determine if the Response is safe and valid to cache. + * + * @param {Request} options.request + * @param {Response} options.response + * @return {Promise} + * + * @private + */ + async _ensureResponseSafeToCache(response) { + let responseToCache = response; + let pluginsUsed = false; + for (const callback of this.iterateCallbacks('cacheWillUpdate')) { + responseToCache = (await callback({ + request: this.request, + response: responseToCache, + event: this.event + })) || undefined; + pluginsUsed = true; + if (!responseToCache) { + break; + } + } + if (!pluginsUsed) { + if (responseToCache && responseToCache.status !== 200) { + responseToCache = undefined; + } + { + if (responseToCache) { + if (responseToCache.status !== 200) { + if (responseToCache.status === 0) { + logger.warn(`The response for '${this.request.url}' ` + `is an opaque response. The caching strategy that you're ` + `using will not cache opaque responses by default.`); + } else { + logger.debug(`The response for '${this.request.url}' ` + `returned a status code of '${response.status}' and won't ` + `be cached as a result.`); + } + } + } + } + } + return responseToCache; + } + } + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * An abstract base class that all other strategy classes must extend from: + * + * @memberof workbox-strategies + */ + class Strategy { + /** + * Creates a new instance of the strategy and sets all documented option + * properties as public instance properties. + * + * Note: if a custom strategy class extends the base Strategy class and does + * not need more than these properties, it does not need to define its own + * constructor. + * + * @param {Object} [options] + * @param {string} [options.cacheName] Cache name to store and retrieve + * requests. Defaults to the cache names provided by + * {@link workbox-core.cacheNames}. + * @param {Array} [options.plugins] [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} + * to use in conjunction with this caching strategy. + * @param {Object} [options.fetchOptions] Values passed along to the + * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) + * of [non-navigation](https://github.com/GoogleChrome/workbox/issues/1796) + * `fetch()` requests made by this strategy. + * @param {Object} [options.matchOptions] The + * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions} + * for any `cache.match()` or `cache.put()` calls made by this strategy. + */ + constructor(options = {}) { + /** + * Cache name to store and retrieve + * requests. Defaults to the cache names provided by + * {@link workbox-core.cacheNames}. + * + * @type {string} + */ + this.cacheName = cacheNames.getRuntimeName(options.cacheName); + /** + * The list + * [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} + * used by this strategy. + * + * @type {Array} + */ + this.plugins = options.plugins || []; + /** + * Values passed along to the + * [`init`]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters} + * of all fetch() requests made by this strategy. + * + * @type {Object} + */ + this.fetchOptions = options.fetchOptions; + /** + * The + * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions} + * for any `cache.match()` or `cache.put()` calls made by this strategy. + * + * @type {Object} + */ + this.matchOptions = options.matchOptions; + } + /** + * Perform a request strategy and returns a `Promise` that will resolve with + * a `Response`, invoking all relevant plugin callbacks. + * + * When a strategy instance is registered with a Workbox + * {@link workbox-routing.Route}, this method is automatically + * called when the route matches. + * + * Alternatively, this method can be used in a standalone `FetchEvent` + * listener by passing it to `event.respondWith()`. + * + * @param {FetchEvent|Object} options A `FetchEvent` or an object with the + * properties listed below. + * @param {Request|string} options.request A request to run this strategy for. + * @param {ExtendableEvent} options.event The event associated with the + * request. + * @param {URL} [options.url] + * @param {*} [options.params] + */ + handle(options) { + const [responseDone] = this.handleAll(options); + return responseDone; + } + /** + * Similar to {@link workbox-strategies.Strategy~handle}, but + * instead of just returning a `Promise` that resolves to a `Response` it + * it will return an tuple of `[response, done]` promises, where the former + * (`response`) is equivalent to what `handle()` returns, and the latter is a + * Promise that will resolve once any promises that were added to + * `event.waitUntil()` as part of performing the strategy have completed. + * + * You can await the `done` promise to ensure any extra work performed by + * the strategy (usually caching responses) completes successfully. + * + * @param {FetchEvent|Object} options A `FetchEvent` or an object with the + * properties listed below. + * @param {Request|string} options.request A request to run this strategy for. + * @param {ExtendableEvent} options.event The event associated with the + * request. + * @param {URL} [options.url] + * @param {*} [options.params] + * @return {Array} A tuple of [response, done] + * promises that can be used to determine when the response resolves as + * well as when the handler has completed all its work. + */ + handleAll(options) { + // Allow for flexible options to be passed. + if (options instanceof FetchEvent) { + options = { + event: options, + request: options.request + }; + } + const event = options.event; + const request = typeof options.request === 'string' ? new Request(options.request) : options.request; + const params = 'params' in options ? options.params : undefined; + const handler = new StrategyHandler(this, { + event, + request, + params + }); + const responseDone = this._getResponse(handler, request, event); + const handlerDone = this._awaitComplete(responseDone, handler, request, event); + // Return an array of promises, suitable for use with Promise.all(). + return [responseDone, handlerDone]; + } + async _getResponse(handler, request, event) { + await handler.runCallbacks('handlerWillStart', { + event, + request + }); + let response = undefined; + try { + response = await this._handle(request, handler); + // The "official" Strategy subclasses all throw this error automatically, + // but in case a third-party Strategy doesn't, ensure that we have a + // consistent failure when there's no response or an error response. + if (!response || response.type === 'error') { + throw new WorkboxError('no-response', { + url: request.url + }); + } + } catch (error) { + if (error instanceof Error) { + for (const callback of handler.iterateCallbacks('handlerDidError')) { + response = await callback({ + error, + event, + request + }); + if (response) { + break; + } + } + } + if (!response) { + throw error; + } else { + logger.log(`While responding to '${getFriendlyURL(request.url)}', ` + `an ${error instanceof Error ? error.toString() : ''} error occurred. Using a fallback response provided by ` + `a handlerDidError plugin.`); + } + } + for (const callback of handler.iterateCallbacks('handlerWillRespond')) { + response = await callback({ + event, + request, + response + }); + } + return response; + } + async _awaitComplete(responseDone, handler, request, event) { + let response; + let error; + try { + response = await responseDone; + } catch (error) { + // Ignore errors, as response errors should be caught via the `response` + // promise above. The `done` promise will only throw for errors in + // promises passed to `handler.waitUntil()`. + } + try { + await handler.runCallbacks('handlerDidRespond', { + event, + request, + response + }); + await handler.doneWaiting(); + } catch (waitUntilError) { + if (waitUntilError instanceof Error) { + error = waitUntilError; + } + } + await handler.runCallbacks('handlerDidComplete', { + event, + request, + response, + error: error + }); + handler.destroy(); + if (error) { + throw error; + } + } + } + /** + * Classes extending the `Strategy` based class should implement this method, + * and leverage the {@link workbox-strategies.StrategyHandler} + * arg to perform all fetching and cache logic, which will ensure all relevant + * cache, cache options, fetch options and plugins are used (per the current + * strategy instance). + * + * @name _handle + * @instance + * @abstract + * @function + * @param {Request} request + * @param {workbox-strategies.StrategyHandler} handler + * @return {Promise} + * + * @memberof workbox-strategies.Strategy + */ + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const messages = { + strategyStart: (strategyName, request) => `Using ${strategyName} to respond to '${getFriendlyURL(request.url)}'`, + printFinalResponse: response => { + if (response) { + logger.groupCollapsed(`View the final response here.`); + logger.log(response || '[No response returned]'); + logger.groupEnd(); + } + } + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * An implementation of a + * [stale-while-revalidate](https://developer.chrome.com/docs/workbox/caching-strategies-overview/#stale-while-revalidate) + * request strategy. + * + * Resources are requested from both the cache and the network in parallel. + * The strategy will respond with the cached version if available, otherwise + * wait for the network response. The cache is updated with the network response + * with each successful request. + * + * By default, this strategy will cache responses with a 200 status code as + * well as [opaque responses](https://developer.chrome.com/docs/workbox/caching-resources-during-runtime/#opaque-responses). + * Opaque responses are cross-origin requests where the response doesn't + * support [CORS](https://enable-cors.org/). + * + * If the network request fails, and there is no cache match, this will throw + * a `WorkboxError` exception. + * + * @extends workbox-strategies.Strategy + * @memberof workbox-strategies + */ + class StaleWhileRevalidate extends Strategy { + /** + * @param {Object} [options] + * @param {string} [options.cacheName] Cache name to store and retrieve + * requests. Defaults to cache names provided by + * {@link workbox-core.cacheNames}. + * @param {Array} [options.plugins] [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins} + * to use in conjunction with this caching strategy. + * @param {Object} [options.fetchOptions] Values passed along to the + * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) + * of [non-navigation](https://github.com/GoogleChrome/workbox/issues/1796) + * `fetch()` requests made by this strategy. + * @param {Object} [options.matchOptions] [`CacheQueryOptions`](https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions) + */ + constructor(options = {}) { + super(options); + // If this instance contains no plugins with a 'cacheWillUpdate' callback, + // prepend the `cacheOkAndOpaquePlugin` plugin to the plugins list. + if (!this.plugins.some(p => 'cacheWillUpdate' in p)) { + this.plugins.unshift(cacheOkAndOpaquePlugin); + } + } + /** + * @private + * @param {Request|string} request A request to run this strategy for. + * @param {workbox-strategies.StrategyHandler} handler The event that + * triggered the request. + * @return {Promise} + */ + async _handle(request, handler) { + const logs = []; + { + finalAssertExports.isInstance(request, Request, { + moduleName: 'workbox-strategies', + className: this.constructor.name, + funcName: 'handle', + paramName: 'request' + }); + } + const fetchAndCachePromise = handler.fetchAndCachePut(request).catch(() => { + // Swallow this error because a 'no-response' error will be thrown in + // main handler return flow. This will be in the `waitUntil()` flow. + }); + void handler.waitUntil(fetchAndCachePromise); + let response = await handler.cacheMatch(request); + let error; + if (response) { + { + logs.push(`Found a cached response in the '${this.cacheName}'` + ` cache. Will update with the network response in the background.`); + } + } else { + { + logs.push(`No response found in the '${this.cacheName}' cache. ` + `Will wait for the network response.`); + } + try { + // NOTE(philipwalton): Really annoying that we have to type cast here. + // https://github.com/microsoft/TypeScript/issues/20006 + response = await fetchAndCachePromise; + } catch (err) { + if (err instanceof Error) { + error = err; + } + } + } + { + logger.groupCollapsed(messages.strategyStart(this.constructor.name, request)); + for (const log of logs) { + logger.log(log); + } + messages.printFinalResponse(response); + logger.groupEnd(); + } + if (!response) { + throw new WorkboxError('no-response', { + url: request.url, + error + }); + } + return response; + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * An implementation of a [cache-first](https://developer.chrome.com/docs/workbox/caching-strategies-overview/#cache-first-falling-back-to-network) + * request strategy. + * + * A cache first strategy is useful for assets that have been revisioned, + * such as URLs like `/styles/example.a8f5f1.css`, since they + * can be cached for long periods of time. + * + * If the network request fails, and there is no cache match, this will throw + * a `WorkboxError` exception. + * + * @extends workbox-strategies.Strategy + * @memberof workbox-strategies + */ + class CacheFirst extends Strategy { + /** + * @private + * @param {Request|string} request A request to run this strategy for. + * @param {workbox-strategies.StrategyHandler} handler The event that + * triggered the request. + * @return {Promise} + */ + async _handle(request, handler) { + const logs = []; + { + finalAssertExports.isInstance(request, Request, { + moduleName: 'workbox-strategies', + className: this.constructor.name, + funcName: 'makeRequest', + paramName: 'request' + }); + } + let response = await handler.cacheMatch(request); + let error = undefined; + if (!response) { + { + logs.push(`No response found in the '${this.cacheName}' cache. ` + `Will respond with a network request.`); + } + try { + response = await handler.fetchAndCachePut(request); + } catch (err) { + if (err instanceof Error) { + error = err; + } + } + { + if (response) { + logs.push(`Got response from network.`); + } else { + logs.push(`Unable to get a response from the network.`); + } + } + } else { + { + logs.push(`Found a cached response in the '${this.cacheName}' cache.`); + } + } + { + logger.groupCollapsed(messages.strategyStart(this.constructor.name, request)); + for (const log of logs) { + logger.log(log); + } + messages.printFinalResponse(response); + logger.groupEnd(); + } + if (!response) { + throw new WorkboxError('no-response', { + url: request.url, + error + }); + } + return response; + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Claim any currently available clients once the service worker + * becomes active. This is normally used in conjunction with `skipWaiting()`. + * + * @memberof workbox-core + */ + function clientsClaim() { + self.addEventListener('activate', () => self.clients.claim()); + } + + /* + Copyright 2020 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A utility method that makes it easier to use `event.waitUntil` with + * async functions and return the result. + * + * @param {ExtendableEvent} event + * @param {Function} asyncFn + * @return {Function} + * @private + */ + function waitUntil(event, asyncFn) { + const returnPromise = asyncFn(); + event.waitUntil(returnPromise); + return returnPromise; + } + + // @ts-ignore + try { + self['workbox:precaching:7.4.0'] && _(); + } catch (e) {} + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + // Name of the search parameter used to store revision info. + const REVISION_SEARCH_PARAM = '__WB_REVISION__'; + /** + * Converts a manifest entry into a versioned URL suitable for precaching. + * + * @param {Object|string} entry + * @return {string} A URL with versioning info. + * + * @private + * @memberof workbox-precaching + */ + function createCacheKey(entry) { + if (!entry) { + throw new WorkboxError('add-to-cache-list-unexpected-type', { + entry + }); + } + // If a precache manifest entry is a string, it's assumed to be a versioned + // URL, like '/app.abcd1234.js'. Return as-is. + if (typeof entry === 'string') { + const urlObject = new URL(entry, location.href); + return { + cacheKey: urlObject.href, + url: urlObject.href + }; + } + const { + revision, + url + } = entry; + if (!url) { + throw new WorkboxError('add-to-cache-list-unexpected-type', { + entry + }); + } + // If there's just a URL and no revision, then it's also assumed to be a + // versioned URL. + if (!revision) { + const urlObject = new URL(url, location.href); + return { + cacheKey: urlObject.href, + url: urlObject.href + }; + } + // Otherwise, construct a properly versioned URL using the custom Workbox + // search parameter along with the revision info. + const cacheKeyURL = new URL(url, location.href); + const originalURL = new URL(url, location.href); + cacheKeyURL.searchParams.set(REVISION_SEARCH_PARAM, revision); + return { + cacheKey: cacheKeyURL.href, + url: originalURL.href + }; + } + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A plugin, designed to be used with PrecacheController, to determine the + * of assets that were updated (or not updated) during the install event. + * + * @private + */ + class PrecacheInstallReportPlugin { + constructor() { + this.updatedURLs = []; + this.notUpdatedURLs = []; + this.handlerWillStart = async ({ + request, + state + }) => { + // TODO: `state` should never be undefined... + if (state) { + state.originalRequest = request; + } + }; + this.cachedResponseWillBeUsed = async ({ + event, + state, + cachedResponse + }) => { + if (event.type === 'install') { + if (state && state.originalRequest && state.originalRequest instanceof Request) { + // TODO: `state` should never be undefined... + const url = state.originalRequest.url; + if (cachedResponse) { + this.notUpdatedURLs.push(url); + } else { + this.updatedURLs.push(url); + } + } + } + return cachedResponse; + }; + } + } + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A plugin, designed to be used with PrecacheController, to translate URLs into + * the corresponding cache key, based on the current revision info. + * + * @private + */ + class PrecacheCacheKeyPlugin { + constructor({ + precacheController + }) { + this.cacheKeyWillBeUsed = async ({ + request, + params + }) => { + // Params is type any, can't change right now. + /* eslint-disable */ + const cacheKey = (params === null || params === void 0 ? void 0 : params.cacheKey) || this._precacheController.getCacheKeyForURL(request.url); + /* eslint-enable */ + return cacheKey ? new Request(cacheKey, { + headers: request.headers + }) : request; + }; + this._precacheController = precacheController; + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * @param {string} groupTitle + * @param {Array} deletedURLs + * + * @private + */ + const logGroup = (groupTitle, deletedURLs) => { + logger.groupCollapsed(groupTitle); + for (const url of deletedURLs) { + logger.log(url); + } + logger.groupEnd(); + }; + /** + * @param {Array} deletedURLs + * + * @private + * @memberof workbox-precaching + */ + function printCleanupDetails(deletedURLs) { + const deletionCount = deletedURLs.length; + if (deletionCount > 0) { + logger.groupCollapsed(`During precaching cleanup, ` + `${deletionCount} cached ` + `request${deletionCount === 1 ? ' was' : 's were'} deleted.`); + logGroup('Deleted Cache Requests', deletedURLs); + logger.groupEnd(); + } + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * @param {string} groupTitle + * @param {Array} urls + * + * @private + */ + function _nestedGroup(groupTitle, urls) { + if (urls.length === 0) { + return; + } + logger.groupCollapsed(groupTitle); + for (const url of urls) { + logger.log(url); + } + logger.groupEnd(); + } + /** + * @param {Array} urlsToPrecache + * @param {Array} urlsAlreadyPrecached + * + * @private + * @memberof workbox-precaching + */ + function printInstallDetails(urlsToPrecache, urlsAlreadyPrecached) { + const precachedCount = urlsToPrecache.length; + const alreadyPrecachedCount = urlsAlreadyPrecached.length; + if (precachedCount || alreadyPrecachedCount) { + let message = `Precaching ${precachedCount} file${precachedCount === 1 ? '' : 's'}.`; + if (alreadyPrecachedCount > 0) { + message += ` ${alreadyPrecachedCount} ` + `file${alreadyPrecachedCount === 1 ? ' is' : 's are'} already cached.`; + } + logger.groupCollapsed(message); + _nestedGroup(`View newly precached URLs.`, urlsToPrecache); + _nestedGroup(`View previously precached URLs.`, urlsAlreadyPrecached); + logger.groupEnd(); + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + let supportStatus; + /** + * A utility function that determines whether the current browser supports + * constructing a new `Response` from a `response.body` stream. + * + * @return {boolean} `true`, if the current browser can successfully + * construct a `Response` from a `response.body` stream, `false` otherwise. + * + * @private + */ + function canConstructResponseFromBodyStream() { + if (supportStatus === undefined) { + const testResponse = new Response(''); + if ('body' in testResponse) { + try { + new Response(testResponse.body); + supportStatus = true; + } catch (error) { + supportStatus = false; + } + } + supportStatus = false; + } + return supportStatus; + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Allows developers to copy a response and modify its `headers`, `status`, + * or `statusText` values (the values settable via a + * [`ResponseInit`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#Syntax} + * object in the constructor). + * To modify these values, pass a function as the second argument. That + * function will be invoked with a single object with the response properties + * `{headers, status, statusText}`. The return value of this function will + * be used as the `ResponseInit` for the new `Response`. To change the values + * either modify the passed parameter(s) and return it, or return a totally + * new object. + * + * This method is intentionally limited to same-origin responses, regardless of + * whether CORS was used or not. + * + * @param {Response} response + * @param {Function} modifier + * @memberof workbox-core + */ + async function copyResponse(response, modifier) { + let origin = null; + // If response.url isn't set, assume it's cross-origin and keep origin null. + if (response.url) { + const responseURL = new URL(response.url); + origin = responseURL.origin; + } + if (origin !== self.location.origin) { + throw new WorkboxError('cross-origin-copy-response', { + origin + }); + } + const clonedResponse = response.clone(); + // Create a fresh `ResponseInit` object by cloning the headers. + const responseInit = { + headers: new Headers(clonedResponse.headers), + status: clonedResponse.status, + statusText: clonedResponse.statusText + }; + // Apply any user modifications. + const modifiedResponseInit = responseInit; + // Create the new response from the body stream and `ResponseInit` + // modifications. Note: not all browsers support the Response.body stream, + // so fall back to reading the entire body into memory as a blob. + const body = canConstructResponseFromBodyStream() ? clonedResponse.body : await clonedResponse.blob(); + return new Response(body, modifiedResponseInit); + } + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A {@link workbox-strategies.Strategy} implementation + * specifically designed to work with + * {@link workbox-precaching.PrecacheController} + * to both cache and fetch precached assets. + * + * Note: an instance of this class is created automatically when creating a + * `PrecacheController`; it's generally not necessary to create this yourself. + * + * @extends workbox-strategies.Strategy + * @memberof workbox-precaching + */ + class PrecacheStrategy extends Strategy { + /** + * + * @param {Object} [options] + * @param {string} [options.cacheName] Cache name to store and retrieve + * requests. Defaults to the cache names provided by + * {@link workbox-core.cacheNames}. + * @param {Array} [options.plugins] {@link https://developers.google.com/web/tools/workbox/guides/using-plugins|Plugins} + * to use in conjunction with this caching strategy. + * @param {Object} [options.fetchOptions] Values passed along to the + * {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters|init} + * of all fetch() requests made by this strategy. + * @param {Object} [options.matchOptions] The + * {@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions|CacheQueryOptions} + * for any `cache.match()` or `cache.put()` calls made by this strategy. + * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to + * get the response from the network if there's a precache miss. + */ + constructor(options = {}) { + options.cacheName = cacheNames.getPrecacheName(options.cacheName); + super(options); + this._fallbackToNetwork = options.fallbackToNetwork === false ? false : true; + // Redirected responses cannot be used to satisfy a navigation request, so + // any redirected response must be "copied" rather than cloned, so the new + // response doesn't contain the `redirected` flag. See: + // https://bugs.chromium.org/p/chromium/issues/detail?id=669363&desc=2#c1 + this.plugins.push(PrecacheStrategy.copyRedirectedCacheableResponsesPlugin); + } + /** + * @private + * @param {Request|string} request A request to run this strategy for. + * @param {workbox-strategies.StrategyHandler} handler The event that + * triggered the request. + * @return {Promise} + */ + async _handle(request, handler) { + const response = await handler.cacheMatch(request); + if (response) { + return response; + } + // If this is an `install` event for an entry that isn't already cached, + // then populate the cache. + if (handler.event && handler.event.type === 'install') { + return await this._handleInstall(request, handler); + } + // Getting here means something went wrong. An entry that should have been + // precached wasn't found in the cache. + return await this._handleFetch(request, handler); + } + async _handleFetch(request, handler) { + let response; + const params = handler.params || {}; + // Fall back to the network if we're configured to do so. + if (this._fallbackToNetwork) { + { + logger.warn(`The precached response for ` + `${getFriendlyURL(request.url)} in ${this.cacheName} was not ` + `found. Falling back to the network.`); + } + const integrityInManifest = params.integrity; + const integrityInRequest = request.integrity; + const noIntegrityConflict = !integrityInRequest || integrityInRequest === integrityInManifest; + // Do not add integrity if the original request is no-cors + // See https://github.com/GoogleChrome/workbox/issues/3096 + response = await handler.fetch(new Request(request, { + integrity: request.mode !== 'no-cors' ? integrityInRequest || integrityInManifest : undefined + })); + // It's only "safe" to repair the cache if we're using SRI to guarantee + // that the response matches the precache manifest's expectations, + // and there's either a) no integrity property in the incoming request + // or b) there is an integrity, and it matches the precache manifest. + // See https://github.com/GoogleChrome/workbox/issues/2858 + // Also if the original request users no-cors we don't use integrity. + // See https://github.com/GoogleChrome/workbox/issues/3096 + if (integrityInManifest && noIntegrityConflict && request.mode !== 'no-cors') { + this._useDefaultCacheabilityPluginIfNeeded(); + const wasCached = await handler.cachePut(request, response.clone()); + { + if (wasCached) { + logger.log(`A response for ${getFriendlyURL(request.url)} ` + `was used to "repair" the precache.`); + } + } + } + } else { + // This shouldn't normally happen, but there are edge cases: + // https://github.com/GoogleChrome/workbox/issues/1441 + throw new WorkboxError('missing-precache-entry', { + cacheName: this.cacheName, + url: request.url + }); + } + { + const cacheKey = params.cacheKey || (await handler.getCacheKey(request, 'read')); + // Workbox is going to handle the route. + // print the routing details to the console. + logger.groupCollapsed(`Precaching is responding to: ` + getFriendlyURL(request.url)); + logger.log(`Serving the precached url: ${getFriendlyURL(cacheKey instanceof Request ? cacheKey.url : cacheKey)}`); + logger.groupCollapsed(`View request details here.`); + logger.log(request); + logger.groupEnd(); + logger.groupCollapsed(`View response details here.`); + logger.log(response); + logger.groupEnd(); + logger.groupEnd(); + } + return response; + } + async _handleInstall(request, handler) { + this._useDefaultCacheabilityPluginIfNeeded(); + const response = await handler.fetch(request); + // Make sure we defer cachePut() until after we know the response + // should be cached; see https://github.com/GoogleChrome/workbox/issues/2737 + const wasCached = await handler.cachePut(request, response.clone()); + if (!wasCached) { + // Throwing here will lead to the `install` handler failing, which + // we want to do if *any* of the responses aren't safe to cache. + throw new WorkboxError('bad-precaching-response', { + url: request.url, + status: response.status + }); + } + return response; + } + /** + * This method is complex, as there a number of things to account for: + * + * The `plugins` array can be set at construction, and/or it might be added to + * to at any time before the strategy is used. + * + * At the time the strategy is used (i.e. during an `install` event), there + * needs to be at least one plugin that implements `cacheWillUpdate` in the + * array, other than `copyRedirectedCacheableResponsesPlugin`. + * + * - If this method is called and there are no suitable `cacheWillUpdate` + * plugins, we need to add `defaultPrecacheCacheabilityPlugin`. + * + * - If this method is called and there is exactly one `cacheWillUpdate`, then + * we don't have to do anything (this might be a previously added + * `defaultPrecacheCacheabilityPlugin`, or it might be a custom plugin). + * + * - If this method is called and there is more than one `cacheWillUpdate`, + * then we need to check if one is `defaultPrecacheCacheabilityPlugin`. If so, + * we need to remove it. (This situation is unlikely, but it could happen if + * the strategy is used multiple times, the first without a `cacheWillUpdate`, + * and then later on after manually adding a custom `cacheWillUpdate`.) + * + * See https://github.com/GoogleChrome/workbox/issues/2737 for more context. + * + * @private + */ + _useDefaultCacheabilityPluginIfNeeded() { + let defaultPluginIndex = null; + let cacheWillUpdatePluginCount = 0; + for (const [index, plugin] of this.plugins.entries()) { + // Ignore the copy redirected plugin when determining what to do. + if (plugin === PrecacheStrategy.copyRedirectedCacheableResponsesPlugin) { + continue; + } + // Save the default plugin's index, in case it needs to be removed. + if (plugin === PrecacheStrategy.defaultPrecacheCacheabilityPlugin) { + defaultPluginIndex = index; + } + if (plugin.cacheWillUpdate) { + cacheWillUpdatePluginCount++; + } + } + if (cacheWillUpdatePluginCount === 0) { + this.plugins.push(PrecacheStrategy.defaultPrecacheCacheabilityPlugin); + } else if (cacheWillUpdatePluginCount > 1 && defaultPluginIndex !== null) { + // Only remove the default plugin; multiple custom plugins are allowed. + this.plugins.splice(defaultPluginIndex, 1); + } + // Nothing needs to be done if cacheWillUpdatePluginCount is 1 + } + } + PrecacheStrategy.defaultPrecacheCacheabilityPlugin = { + async cacheWillUpdate({ + response + }) { + if (!response || response.status >= 400) { + return null; + } + return response; + } + }; + PrecacheStrategy.copyRedirectedCacheableResponsesPlugin = { + async cacheWillUpdate({ + response + }) { + return response.redirected ? await copyResponse(response) : response; + } + }; + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Performs efficient precaching of assets. + * + * @memberof workbox-precaching + */ + class PrecacheController { + /** + * Create a new PrecacheController. + * + * @param {Object} [options] + * @param {string} [options.cacheName] The cache to use for precaching. + * @param {string} [options.plugins] Plugins to use when precaching as well + * as responding to fetch events for precached assets. + * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to + * get the response from the network if there's a precache miss. + */ + constructor({ + cacheName, + plugins = [], + fallbackToNetwork = true + } = {}) { + this._urlsToCacheKeys = new Map(); + this._urlsToCacheModes = new Map(); + this._cacheKeysToIntegrities = new Map(); + this._strategy = new PrecacheStrategy({ + cacheName: cacheNames.getPrecacheName(cacheName), + plugins: [...plugins, new PrecacheCacheKeyPlugin({ + precacheController: this + })], + fallbackToNetwork + }); + // Bind the install and activate methods to the instance. + this.install = this.install.bind(this); + this.activate = this.activate.bind(this); + } + /** + * @type {workbox-precaching.PrecacheStrategy} The strategy created by this controller and + * used to cache assets and respond to fetch events. + */ + get strategy() { + return this._strategy; + } + /** + * Adds items to the precache list, removing any duplicates and + * stores the files in the + * {@link workbox-core.cacheNames|"precache cache"} when the service + * worker installs. + * + * This method can be called multiple times. + * + * @param {Array} [entries=[]] Array of entries to precache. + */ + precache(entries) { + this.addToCacheList(entries); + if (!this._installAndActiveListenersAdded) { + self.addEventListener('install', this.install); + self.addEventListener('activate', this.activate); + this._installAndActiveListenersAdded = true; + } + } + /** + * This method will add items to the precache list, removing duplicates + * and ensuring the information is valid. + * + * @param {Array} entries + * Array of entries to precache. + */ + addToCacheList(entries) { + { + finalAssertExports.isArray(entries, { + moduleName: 'workbox-precaching', + className: 'PrecacheController', + funcName: 'addToCacheList', + paramName: 'entries' + }); + } + const urlsToWarnAbout = []; + for (const entry of entries) { + // See https://github.com/GoogleChrome/workbox/issues/2259 + if (typeof entry === 'string') { + urlsToWarnAbout.push(entry); + } else if (entry && entry.revision === undefined) { + urlsToWarnAbout.push(entry.url); + } + const { + cacheKey, + url + } = createCacheKey(entry); + const cacheMode = typeof entry !== 'string' && entry.revision ? 'reload' : 'default'; + if (this._urlsToCacheKeys.has(url) && this._urlsToCacheKeys.get(url) !== cacheKey) { + throw new WorkboxError('add-to-cache-list-conflicting-entries', { + firstEntry: this._urlsToCacheKeys.get(url), + secondEntry: cacheKey + }); + } + if (typeof entry !== 'string' && entry.integrity) { + if (this._cacheKeysToIntegrities.has(cacheKey) && this._cacheKeysToIntegrities.get(cacheKey) !== entry.integrity) { + throw new WorkboxError('add-to-cache-list-conflicting-integrities', { + url + }); + } + this._cacheKeysToIntegrities.set(cacheKey, entry.integrity); + } + this._urlsToCacheKeys.set(url, cacheKey); + this._urlsToCacheModes.set(url, cacheMode); + if (urlsToWarnAbout.length > 0) { + const warningMessage = `Workbox is precaching URLs without revision ` + `info: ${urlsToWarnAbout.join(', ')}\nThis is generally NOT safe. ` + `Learn more at https://bit.ly/wb-precache`; + { + logger.warn(warningMessage); + } + } + } + } + /** + * Precaches new and updated assets. Call this method from the service worker + * install event. + * + * Note: this method calls `event.waitUntil()` for you, so you do not need + * to call it yourself in your event handlers. + * + * @param {ExtendableEvent} event + * @return {Promise} + */ + install(event) { + // waitUntil returns Promise + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return waitUntil(event, async () => { + const installReportPlugin = new PrecacheInstallReportPlugin(); + this.strategy.plugins.push(installReportPlugin); + // Cache entries one at a time. + // See https://github.com/GoogleChrome/workbox/issues/2528 + for (const [url, cacheKey] of this._urlsToCacheKeys) { + const integrity = this._cacheKeysToIntegrities.get(cacheKey); + const cacheMode = this._urlsToCacheModes.get(url); + const request = new Request(url, { + integrity, + cache: cacheMode, + credentials: 'same-origin' + }); + await Promise.all(this.strategy.handleAll({ + params: { + cacheKey + }, + request, + event + })); + } + const { + updatedURLs, + notUpdatedURLs + } = installReportPlugin; + { + printInstallDetails(updatedURLs, notUpdatedURLs); + } + return { + updatedURLs, + notUpdatedURLs + }; + }); + } + /** + * Deletes assets that are no longer present in the current precache manifest. + * Call this method from the service worker activate event. + * + * Note: this method calls `event.waitUntil()` for you, so you do not need + * to call it yourself in your event handlers. + * + * @param {ExtendableEvent} event + * @return {Promise} + */ + activate(event) { + // waitUntil returns Promise + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return waitUntil(event, async () => { + const cache = await self.caches.open(this.strategy.cacheName); + const currentlyCachedRequests = await cache.keys(); + const expectedCacheKeys = new Set(this._urlsToCacheKeys.values()); + const deletedURLs = []; + for (const request of currentlyCachedRequests) { + if (!expectedCacheKeys.has(request.url)) { + await cache.delete(request); + deletedURLs.push(request.url); + } + } + { + printCleanupDetails(deletedURLs); + } + return { + deletedURLs + }; + }); + } + /** + * Returns a mapping of a precached URL to the corresponding cache key, taking + * into account the revision information for the URL. + * + * @return {Map} A URL to cache key mapping. + */ + getURLsToCacheKeys() { + return this._urlsToCacheKeys; + } + /** + * Returns a list of all the URLs that have been precached by the current + * service worker. + * + * @return {Array} The precached URLs. + */ + getCachedURLs() { + return [...this._urlsToCacheKeys.keys()]; + } + /** + * Returns the cache key used for storing a given URL. If that URL is + * unversioned, like `/index.html', then the cache key will be the original + * URL with a search parameter appended to it. + * + * @param {string} url A URL whose cache key you want to look up. + * @return {string} The versioned URL that corresponds to a cache key + * for the original URL, or undefined if that URL isn't precached. + */ + getCacheKeyForURL(url) { + const urlObject = new URL(url, location.href); + return this._urlsToCacheKeys.get(urlObject.href); + } + /** + * @param {string} url A cache key whose SRI you want to look up. + * @return {string} The subresource integrity associated with the cache key, + * or undefined if it's not set. + */ + getIntegrityForCacheKey(cacheKey) { + return this._cacheKeysToIntegrities.get(cacheKey); + } + /** + * This acts as a drop-in replacement for + * [`cache.match()`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/match) + * with the following differences: + * + * - It knows what the name of the precache is, and only checks in that cache. + * - It allows you to pass in an "original" URL without versioning parameters, + * and it will automatically look up the correct cache key for the currently + * active revision of that URL. + * + * E.g., `matchPrecache('index.html')` will find the correct precached + * response for the currently active service worker, even if the actual cache + * key is `'/index.html?__WB_REVISION__=1234abcd'`. + * + * @param {string|Request} request The key (without revisioning parameters) + * to look up in the precache. + * @return {Promise} + */ + async matchPrecache(request) { + const url = request instanceof Request ? request.url : request; + const cacheKey = this.getCacheKeyForURL(url); + if (cacheKey) { + const cache = await self.caches.open(this.strategy.cacheName); + return cache.match(cacheKey); + } + return undefined; + } + /** + * Returns a function that looks up `url` in the precache (taking into + * account revision information), and returns the corresponding `Response`. + * + * @param {string} url The precached URL which will be used to lookup the + * `Response`. + * @return {workbox-routing~handlerCallback} + */ + createHandlerBoundToURL(url) { + const cacheKey = this.getCacheKeyForURL(url); + if (!cacheKey) { + throw new WorkboxError('non-precached-url', { + url + }); + } + return options => { + options.request = new Request(url); + options.params = Object.assign({ + cacheKey + }, options.params); + return this.strategy.handle(options); + }; + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + let precacheController; + /** + * @return {PrecacheController} + * @private + */ + const getOrCreatePrecacheController = () => { + if (!precacheController) { + precacheController = new PrecacheController(); + } + return precacheController; + }; + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Removes any URL search parameters that should be ignored. + * + * @param {URL} urlObject The original URL. + * @param {Array} ignoreURLParametersMatching RegExps to test against + * each search parameter name. Matches mean that the search parameter should be + * ignored. + * @return {URL} The URL with any ignored search parameters removed. + * + * @private + * @memberof workbox-precaching + */ + function removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching = []) { + // Convert the iterable into an array at the start of the loop to make sure + // deletion doesn't mess up iteration. + for (const paramName of [...urlObject.searchParams.keys()]) { + if (ignoreURLParametersMatching.some(regExp => regExp.test(paramName))) { + urlObject.searchParams.delete(paramName); + } + } + return urlObject; + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Generator function that yields possible variations on the original URL to + * check, one at a time. + * + * @param {string} url + * @param {Object} options + * + * @private + * @memberof workbox-precaching + */ + function* generateURLVariations(url, { + ignoreURLParametersMatching = [/^utm_/, /^fbclid$/], + directoryIndex = 'index.html', + cleanURLs = true, + urlManipulation + } = {}) { + const urlObject = new URL(url, location.href); + urlObject.hash = ''; + yield urlObject.href; + const urlWithoutIgnoredParams = removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching); + yield urlWithoutIgnoredParams.href; + if (directoryIndex && urlWithoutIgnoredParams.pathname.endsWith('/')) { + const directoryURL = new URL(urlWithoutIgnoredParams.href); + directoryURL.pathname += directoryIndex; + yield directoryURL.href; + } + if (cleanURLs) { + const cleanURL = new URL(urlWithoutIgnoredParams.href); + cleanURL.pathname += '.html'; + yield cleanURL.href; + } + if (urlManipulation) { + const additionalURLs = urlManipulation({ + url: urlObject + }); + for (const urlToAttempt of additionalURLs) { + yield urlToAttempt.href; + } + } + } + + /* + Copyright 2020 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * A subclass of {@link workbox-routing.Route} that takes a + * {@link workbox-precaching.PrecacheController} + * instance and uses it to match incoming requests and handle fetching + * responses from the precache. + * + * @memberof workbox-precaching + * @extends workbox-routing.Route + */ + class PrecacheRoute extends Route { + /** + * @param {PrecacheController} precacheController A `PrecacheController` + * instance used to both match requests and respond to fetch events. + * @param {Object} [options] Options to control how requests are matched + * against the list of precached URLs. + * @param {string} [options.directoryIndex=index.html] The `directoryIndex` will + * check cache entries for a URLs ending with '/' to see if there is a hit when + * appending the `directoryIndex` value. + * @param {Array} [options.ignoreURLParametersMatching=[/^utm_/, /^fbclid$/]] An + * array of regex's to remove search params when looking for a cache match. + * @param {boolean} [options.cleanURLs=true] The `cleanURLs` option will + * check the cache for the URL with a `.html` added to the end of the end. + * @param {workbox-precaching~urlManipulation} [options.urlManipulation] + * This is a function that should take a URL and return an array of + * alternative URLs that should be checked for precache matches. + */ + constructor(precacheController, options) { + const match = ({ + request + }) => { + const urlsToCacheKeys = precacheController.getURLsToCacheKeys(); + for (const possibleURL of generateURLVariations(request.url, options)) { + const cacheKey = urlsToCacheKeys.get(possibleURL); + if (cacheKey) { + const integrity = precacheController.getIntegrityForCacheKey(cacheKey); + return { + cacheKey, + integrity + }; + } + } + { + logger.debug(`Precaching did not find a match for ` + getFriendlyURL(request.url)); + } + return; + }; + super(match, precacheController.strategy); + } + } + + /* + Copyright 2019 Google LLC + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Add a `fetch` listener to the service worker that will + * respond to + * [network requests]{@link https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#Custom_responses_to_requests} + * with precached assets. + * + * Requests for assets that aren't precached, the `FetchEvent` will not be + * responded to, allowing the event to fall through to other `fetch` event + * listeners. + * + * @param {Object} [options] See the {@link workbox-precaching.PrecacheRoute} + * options. + * + * @memberof workbox-precaching + */ + function addRoute(options) { + const precacheController = getOrCreatePrecacheController(); + const precacheRoute = new PrecacheRoute(precacheController, options); + registerRoute(precacheRoute); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Adds items to the precache list, removing any duplicates and + * stores the files in the + * {@link workbox-core.cacheNames|"precache cache"} when the service + * worker installs. + * + * This method can be called multiple times. + * + * Please note: This method **will not** serve any of the cached files for you. + * It only precaches files. To respond to a network request you call + * {@link workbox-precaching.addRoute}. + * + * If you have a single array of files to precache, you can just call + * {@link workbox-precaching.precacheAndRoute}. + * + * @param {Array} [entries=[]] Array of entries to precache. + * + * @memberof workbox-precaching + */ + function precache(entries) { + const precacheController = getOrCreatePrecacheController(); + precacheController.precache(entries); + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * This method will add entries to the precache list and add a route to + * respond to fetch events. + * + * This is a convenience method that will call + * {@link workbox-precaching.precache} and + * {@link workbox-precaching.addRoute} in a single call. + * + * @param {Array} entries Array of entries to precache. + * @param {Object} [options] See the + * {@link workbox-precaching.PrecacheRoute} options. + * + * @memberof workbox-precaching + */ + function precacheAndRoute(entries, options) { + precache(entries); + addRoute(options); + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + const SUBSTRING_TO_FIND = '-precache-'; + /** + * Cleans up incompatible precaches that were created by older versions of + * Workbox, by a service worker registered under the current scope. + * + * This is meant to be called as part of the `activate` event. + * + * This should be safe to use as long as you don't include `substringToFind` + * (defaulting to `-precache-`) in your non-precache cache names. + * + * @param {string} currentPrecacheName The cache name currently in use for + * precaching. This cache won't be deleted. + * @param {string} [substringToFind='-precache-'] Cache names which include this + * substring will be deleted (excluding `currentPrecacheName`). + * @return {Array} A list of all the cache names that were deleted. + * + * @private + * @memberof workbox-precaching + */ + const deleteOutdatedCaches = async (currentPrecacheName, substringToFind = SUBSTRING_TO_FIND) => { + const cacheNames = await self.caches.keys(); + const cacheNamesToDelete = cacheNames.filter(cacheName => { + return cacheName.includes(substringToFind) && cacheName.includes(self.registration.scope) && cacheName !== currentPrecacheName; + }); + await Promise.all(cacheNamesToDelete.map(cacheName => self.caches.delete(cacheName))); + return cacheNamesToDelete; + }; + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Adds an `activate` event listener which will clean up incompatible + * precaches that were created by older versions of Workbox. + * + * @memberof workbox-precaching + */ + function cleanupOutdatedCaches() { + // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705 + self.addEventListener('activate', event => { + const cacheName = cacheNames.getPrecacheName(); + event.waitUntil(deleteOutdatedCaches(cacheName).then(cachesDeleted => { + { + if (cachesDeleted.length > 0) { + logger.log(`The following out-of-date precaches were cleaned up ` + `automatically:`, cachesDeleted); + } + } + })); + }); + } + + /* + Copyright 2018 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * NavigationRoute makes it easy to create a + * {@link workbox-routing.Route} that matches for browser + * [navigation requests]{@link https://developers.google.com/web/fundamentals/primers/service-workers/high-performance-loading#first_what_are_navigation_requests}. + * + * It will only match incoming Requests whose + * {@link https://fetch.spec.whatwg.org/#concept-request-mode|mode} + * is set to `navigate`. + * + * You can optionally only apply this route to a subset of navigation requests + * by using one or both of the `denylist` and `allowlist` parameters. + * + * @memberof workbox-routing + * @extends workbox-routing.Route + */ + class NavigationRoute extends Route { + /** + * If both `denylist` and `allowlist` are provided, the `denylist` will + * take precedence and the request will not match this route. + * + * The regular expressions in `allowlist` and `denylist` + * are matched against the concatenated + * [`pathname`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/pathname} + * and [`search`]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLHyperlinkElementUtils/search} + * portions of the requested URL. + * + * *Note*: These RegExps may be evaluated against every destination URL during + * a navigation. Avoid using + * [complex RegExps](https://github.com/GoogleChrome/workbox/issues/3077), + * or else your users may see delays when navigating your site. + * + * @param {workbox-routing~handlerCallback} handler A callback + * function that returns a Promise resulting in a Response. + * @param {Object} options + * @param {Array} [options.denylist] If any of these patterns match, + * the route will not handle the request (even if a allowlist RegExp matches). + * @param {Array} [options.allowlist=[/./]] If any of these patterns + * match the URL's pathname and search parameter, the route will handle the + * request (assuming the denylist doesn't match). + */ + constructor(handler, { + allowlist = [/./], + denylist = [] + } = {}) { + { + finalAssertExports.isArrayOfClass(allowlist, RegExp, { + moduleName: 'workbox-routing', + className: 'NavigationRoute', + funcName: 'constructor', + paramName: 'options.allowlist' + }); + finalAssertExports.isArrayOfClass(denylist, RegExp, { + moduleName: 'workbox-routing', + className: 'NavigationRoute', + funcName: 'constructor', + paramName: 'options.denylist' + }); + } + super(options => this._match(options), handler); + this._allowlist = allowlist; + this._denylist = denylist; + } + /** + * Routes match handler. + * + * @param {Object} options + * @param {URL} options.url + * @param {Request} options.request + * @return {boolean} + * + * @private + */ + _match({ + url, + request + }) { + if (request && request.mode !== 'navigate') { + return false; + } + const pathnameAndSearch = url.pathname + url.search; + for (const regExp of this._denylist) { + if (regExp.test(pathnameAndSearch)) { + { + logger.log(`The navigation route ${pathnameAndSearch} is not ` + `being used, since the URL matches this denylist pattern: ` + `${regExp.toString()}`); + } + return false; + } + } + if (this._allowlist.some(regExp => regExp.test(pathnameAndSearch))) { + { + logger.debug(`The navigation route ${pathnameAndSearch} ` + `is being used.`); + } + return true; + } + { + logger.log(`The navigation route ${pathnameAndSearch} is not ` + `being used, since the URL being navigated to doesn't ` + `match the allowlist.`); + } + return false; + } + } + + /* + Copyright 2019 Google LLC + + Use of this source code is governed by an MIT-style + license that can be found in the LICENSE file or at + https://opensource.org/licenses/MIT. + */ + /** + * Helper function that calls + * {@link PrecacheController#createHandlerBoundToURL} on the default + * {@link PrecacheController} instance. + * + * If you are creating your own {@link PrecacheController}, then call the + * {@link PrecacheController#createHandlerBoundToURL} on that instance, + * instead of using this function. + * + * @param {string} url The precached URL which will be used to lookup the + * `Response`. + * @param {boolean} [fallbackToNetwork=true] Whether to attempt to get the + * response from the network if there's a precache miss. + * @return {workbox-routing~handlerCallback} + * + * @memberof workbox-precaching + */ + function createHandlerBoundToURL(url) { + const precacheController = getOrCreatePrecacheController(); + return precacheController.createHandlerBoundToURL(url); + } + + exports.CacheFirst = CacheFirst; + exports.CacheableResponsePlugin = CacheableResponsePlugin; + exports.ExpirationPlugin = ExpirationPlugin; + exports.NavigationRoute = NavigationRoute; + exports.StaleWhileRevalidate = StaleWhileRevalidate; + exports.cleanupOutdatedCaches = cleanupOutdatedCaches; + exports.clientsClaim = clientsClaim; + exports.createHandlerBoundToURL = createHandlerBoundToURL; + exports.precacheAndRoute = precacheAndRoute; + exports.registerRoute = registerRoute; + +})); diff --git a/index.html b/index.html index 7dd2309..8f358ca 100644 --- a/index.html +++ b/index.html @@ -2,16 +2,18 @@ - - - RestroAI Operations Platform - Kitchen & Analytics Suite - + + + + + + RestroAI — Table Ordering
- + diff --git a/package-lock.json b/package-lock.json index ee3a060..ec878f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,10 @@ "@mui/icons-material": "^9.2.0", "@mui/material": "^9.2.0", "react": "^19.2.7", - "react-dom": "^19.2.7" + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.2", + "vite-plugin-pwa": "^1.3.0", + "workbox-window": "^7.4.1" }, "devDependencies": { "@types/node": "^24.13.2", @@ -25,6 +28,22 @@ "vite": "^8.1.1" } }, + "node_modules/@apideck/better-ajv-errors": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.7.tgz", + "integrity": "sha512-TajUJwGWbDwkCx/CZi7tRE8PVB7simCvKJfHUsSdvps+aTM/PDPP4gkLmKnc+x3CE//y9i/nj74GqdL/hwk7Iw==", + "license": "MIT", + "dependencies": { + "jsonpointer": "^5.0.1", + "leven": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "ajv": ">=8" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "license": "MIT", @@ -37,12 +56,59 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/generator": { + "node_modules/@babel/compat-data": { "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -51,6 +117,88 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", + "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", + "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.29.7", "license": "MIT", @@ -58,6 +206,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.29.7", "license": "MIT", @@ -69,6 +230,91 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", + "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-wrap-function": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "license": "MIT", @@ -83,12 +329,50 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/parser": { + "node_modules/@babel/helper-validator-option": { "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", + "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", "license": "MIT", "dependencies": { + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -96,6 +380,1063 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", + "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", + "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", + "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", + "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", + "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", + "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", + "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", + "integrity": "sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", + "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", + "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", + "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-remap-async-to-generator": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", + "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", + "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", + "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", + "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", + "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/template": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", + "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", + "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", + "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", + "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", + "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", + "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", + "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", + "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", + "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", + "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", + "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", + "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", + "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", + "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.8.tgz", + "integrity": "sha512-6iSnEK0zlkLKU4heofK/AdmRD4e2SHVpJMtrwnTCzhnaM98ria4rTrOXBBi45BTTYnJtO8txnPsX4fChYXkmeA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.8" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", + "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", + "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", + "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", + "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", + "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", + "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", + "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", + "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", + "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", + "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.8.tgz", + "integrity": "sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", + "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", + "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", + "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.8.tgz", + "integrity": "sha512-4S9ksMGVWUshvgK0mKfvZky7leuG5/uoFVwMpAomJ8bMoDJiNHRVmc1EglwW/CmGVSqqWpEbXm9FmbRit22qoA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", + "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", + "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", + "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz", + "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", + "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", + "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", + "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.7.tgz", + "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", + "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.29.7", + "@babel/plugin-syntax-import-attributes": "^7.29.7", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.29.7", + "@babel/plugin-transform-async-generator-functions": "^7.29.7", + "@babel/plugin-transform-async-to-generator": "^7.29.7", + "@babel/plugin-transform-block-scoped-functions": "^7.29.7", + "@babel/plugin-transform-block-scoping": "^7.29.7", + "@babel/plugin-transform-class-properties": "^7.29.7", + "@babel/plugin-transform-class-static-block": "^7.29.7", + "@babel/plugin-transform-classes": "^7.29.7", + "@babel/plugin-transform-computed-properties": "^7.29.7", + "@babel/plugin-transform-destructuring": "^7.29.7", + "@babel/plugin-transform-dotall-regex": "^7.29.7", + "@babel/plugin-transform-duplicate-keys": "^7.29.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-dynamic-import": "^7.29.7", + "@babel/plugin-transform-explicit-resource-management": "^7.29.7", + "@babel/plugin-transform-exponentiation-operator": "^7.29.7", + "@babel/plugin-transform-export-namespace-from": "^7.29.7", + "@babel/plugin-transform-for-of": "^7.29.7", + "@babel/plugin-transform-function-name": "^7.29.7", + "@babel/plugin-transform-json-strings": "^7.29.7", + "@babel/plugin-transform-literals": "^7.29.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", + "@babel/plugin-transform-member-expression-literals": "^7.29.7", + "@babel/plugin-transform-modules-amd": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-modules-systemjs": "^7.29.7", + "@babel/plugin-transform-modules-umd": "^7.29.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", + "@babel/plugin-transform-new-target": "^7.29.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", + "@babel/plugin-transform-numeric-separator": "^7.29.7", + "@babel/plugin-transform-object-rest-spread": "^7.29.7", + "@babel/plugin-transform-object-super": "^7.29.7", + "@babel/plugin-transform-optional-catch-binding": "^7.29.7", + "@babel/plugin-transform-optional-chaining": "^7.29.7", + "@babel/plugin-transform-parameters": "^7.29.7", + "@babel/plugin-transform-private-methods": "^7.29.7", + "@babel/plugin-transform-private-property-in-object": "^7.29.7", + "@babel/plugin-transform-property-literals": "^7.29.7", + "@babel/plugin-transform-regenerator": "^7.29.7", + "@babel/plugin-transform-regexp-modifiers": "^7.29.7", + "@babel/plugin-transform-reserved-words": "^7.29.7", + "@babel/plugin-transform-shorthand-properties": "^7.29.7", + "@babel/plugin-transform-spread": "^7.29.7", + "@babel/plugin-transform-sticky-regex": "^7.29.7", + "@babel/plugin-transform-template-literals": "^7.29.7", + "@babel/plugin-transform-typeof-symbol": "^7.29.7", + "@babel/plugin-transform-unicode-escapes": "^7.29.7", + "@babel/plugin-transform-unicode-property-regex": "^7.29.7", + "@babel/plugin-transform-unicode-regex": "^7.29.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.7", "license": "MIT", @@ -116,15 +1457,17 @@ } }, "node_modules/@babel/traverse": { - "version": "7.29.7", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", - "@babel/generator": "^7.29.7", + "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", - "@babel/parser": "^7.29.7", + "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", - "@babel/types": "^7.29.7", + "@babel/types": "^7.29.8", "debug": "^4.3.1" }, "engines": { @@ -132,7 +1475,9 @@ } }, "node_modules/@babel/types": { - "version": "7.29.7", + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -146,7 +1491,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -158,7 +1502,6 @@ "version": "1.11.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -169,7 +1512,6 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -296,6 +1638,15 @@ "version": "0.4.0", "license": "MIT" }, + "node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "license": "MIT", @@ -304,6 +1655,16 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "license": "MIT", @@ -311,6 +1672,16 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "license": "MIT" @@ -542,11 +1913,26 @@ } } }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -566,7 +1952,6 @@ }, "node_modules/@oxc-project/types": { "version": "0.139.0", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" @@ -699,9 +2084,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -719,9 +2101,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -739,9 +2118,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -759,9 +2135,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -779,9 +2152,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -799,9 +2169,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -819,9 +2186,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -839,9 +2203,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -932,7 +2293,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -949,7 +2309,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -966,7 +2325,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -983,7 +2341,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1000,7 +2357,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1017,10 +2373,6 @@ "cpu": [ "arm64" ], - "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1037,10 +2389,6 @@ "cpu": [ "arm64" ], - "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1057,10 +2405,6 @@ "cpu": [ "ppc64" ], - "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1077,10 +2421,6 @@ "cpu": [ "s390x" ], - "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1097,10 +2437,6 @@ "cpu": [ "x64" ], - "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1117,10 +2453,6 @@ "cpu": [ "x64" ], - "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1137,7 +2469,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1154,7 +2485,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1173,7 +2503,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1188,7 +2517,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -1200,23 +2528,482 @@ }, "node_modules/@rolldown/pluginutils": { "version": "1.0.1", - "dev": true, "license": "MIT" }, + "node_modules/@rollup/plugin-babel": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-6.1.0.tgz", + "integrity": "sha512-dFZNuFD2YRcoomP4oYf+DvQNSUA9ih+A3vUqopQx5EdtPGo3WBnQcI/S8pwpz91UsGfL0HsMSOlaMld8HrbubA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-16.0.3.tgz", + "integrity": "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-6.0.3.tgz", + "integrity": "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA==", + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-1.0.0.tgz", + "integrity": "sha512-FnCxhTBx6bMOYQrar6C8h3scPt8/JwIzw3+AJ2K++6guogH5fYaIFia+zZuhqv0eo1RN7W1Pz630SyvLbDjhtQ==", + "license": "MIT", + "dependencies": { + "serialize-javascript": "^7.0.3", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@trickfilm400/rollup-plugin-off-main-thread": { + "version": "3.0.0-pre1", + "resolved": "https://registry.npmjs.org/@trickfilm400/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-3.0.0-pre1.tgz", + "integrity": "sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==", + "license": "Apache-2.0", + "dependencies": { + "ejs": "^3.1.10", + "json5": "^2.2.3", + "magic-string": "^0.30.21", + "string.prototype.matchall": "^4.0.12" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.13.3", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" @@ -1252,6 +3039,18 @@ "@types/react": "*" } }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT" + }, "node_modules/@vitejs/plugin-react": { "version": "6.0.4", "dev": true, @@ -1276,6 +3075,110 @@ } } }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "license": "MIT", @@ -1289,6 +3192,164 @@ "npm": ">=6" } }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", + "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.8", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", + "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", + "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.8" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.13", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.13.tgz", + "integrity": "sha512-k9HNuUVMlqVjQ9UHzfPjIqiDbWw7WqT1AoT7GL8VwvF3r0ZfArtgiSPAlmupyNquNgOJHTuH4CKYf8ttMTWBTQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "license": "MIT", @@ -1296,6 +3357,26 @@ "node": ">=6" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/clsx": { "version": "2.1.1", "license": "MIT", @@ -1303,10 +3384,54 @@ "node": ">=6" } }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/common-tags": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/common-tags/-/common-tags-1.8.2.tgz", + "integrity": "sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/core-js-compat": { + "version": "3.50.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.50.0.tgz", + "integrity": "sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==", + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.7" + }, + "engines": { + "node": ">=6.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "license": "MIT", @@ -1328,10 +3453,84 @@ "node": ">= 6" } }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/csstype": { "version": "3.2.3", "license": "MIT" }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.4.3", "license": "MIT", @@ -1347,9 +3546,51 @@ } } }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/detect-libc": { "version": "2.1.2", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -1363,6 +3604,41 @@ "csstype": "^3.0.2" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.405", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.405.tgz", + "integrity": "sha512-bNglH7lPH5l+yHOes7Zr4VqxhOy4BQ9ZBUX4VdoFgxMpzJk7W1ZoO3Vgd9Pxa9PyjQ76sfm2aKH/nzEcCNRlew==", + "license": "ISC" + }, "node_modules/error-ex": { "version": "1.3.4", "license": "MIT", @@ -1370,6 +3646,101 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-abstract": { + "version": "1.24.2", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", + "integrity": "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==", + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-errors": { "version": "1.3.0", "license": "MIT", @@ -1377,6 +3748,62 @@ "node": ">= 0.4" } }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", + "license": "MIT", + "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "is-callable": "^1.2.7", + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "license": "MIT", @@ -1387,9 +3814,63 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eta": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-4.6.0.tgz", + "integrity": "sha512-lW6is4T1NFOYnmqGZIfvixqj7A7sSvScF+DN8EK6K58xI5MZ5UvYe0GjopxOXQtZvUn4eDdVuZ8XSoYWTMEKwA==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/bgub/eta?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fdir": { "version": "6.5.0", - "dev": true, "license": "MIT", "engines": { "node": ">=12.0.0" @@ -1403,15 +3884,96 @@ } } }, + "node_modules/filelist": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", + "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/find-root": { "version": "1.1.0", "license": "MIT" }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -1429,6 +3991,240 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-own-enumerable-property-symbols": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", + "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", + "license": "ISC" + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.4", "license": "MIT", @@ -1450,6 +4246,12 @@ "version": "16.13.1", "license": "MIT" }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, "node_modules/import-fresh": { "version": "3.3.1", "license": "MIT", @@ -1464,10 +4266,103 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "license": "MIT" }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.16.2", "license": "MIT", @@ -1481,6 +4376,344 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "license": "MIT" + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", + "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jake": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.6", + "filelist": "^1.0.4", + "picocolors": "^1.1.1" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "license": "MIT" @@ -1499,9 +4732,56 @@ "version": "2.3.1", "license": "MIT" }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonfile": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonpointer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz", + "integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/lightningcss": { "version": "1.33.0", - "dev": true, "license": "MPL-2.0", "dependencies": { "detect-libc": "^2.0.3" @@ -1534,7 +4814,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1555,7 +4834,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1576,7 +4854,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1597,7 +4874,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1618,7 +4894,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1639,10 +4914,6 @@ "cpu": [ "arm64" ], - "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1663,10 +4934,6 @@ "cpu": [ "arm64" ], - "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1687,10 +4954,6 @@ "cpu": [ "x64" ], - "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1711,10 +4974,6 @@ "cpu": [ "x64" ], - "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1735,7 +4994,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1754,7 +5012,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1772,6 +5029,12 @@ "version": "1.2.4", "license": "MIT" }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, "node_modules/loose-envify": { "version": "1.4.0", "license": "MIT", @@ -1782,13 +5045,63 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/ms": { "version": "2.1.3", "license": "MIT" }, "node_modules/nanoid": { "version": "3.3.16", - "dev": true, "funding": [ { "type": "github", @@ -1803,6 +5116,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/object-assign": { "version": "4.1.1", "license": "MIT", @@ -1810,6 +5132,65 @@ "node": ">=0.10.0" } }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/own-keys": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/oxlint": { "version": "1.76.0", "dev": true, @@ -1857,6 +5238,12 @@ } } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "license": "MIT", @@ -1883,10 +5270,44 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-parse": { "version": "1.0.7", "license": "MIT" }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/path-type": { "version": "4.0.0", "license": "MIT", @@ -1900,7 +5321,6 @@ }, "node_modules/picomatch": { "version": "4.0.5", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1909,9 +5329,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/postcss": { "version": "8.5.24", - "dev": true, "funding": [ { "type": "opencollective", @@ -1936,6 +5364,18 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prop-types": { "version": "15.8.1", "license": "MIT", @@ -1970,6 +5410,44 @@ "version": "19.2.8", "license": "MIT" }, + "node_modules/react-router": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", + "license": "MIT", + "dependencies": { + "react-router": "7.18.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, "node_modules/react-transition-group": { "version": "4.4.5", "license": "BSD-3-Clause", @@ -1984,6 +5462,110 @@ "react-dom": ">=16.6.0" } }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "license": "MIT" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "license": "MIT" + }, + "node_modules/regjsparser": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.2.tgz", + "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve": { "version": "1.22.12", "license": "MIT", @@ -2012,7 +5594,6 @@ }, "node_modules/rolldown": { "version": "1.1.5", - "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.139.0", @@ -2042,10 +5623,291 @@ "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.4.tgz", + "integrity": "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/scheduler": { "version": "0.27.0", "license": "MIT" }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.1.0.tgz", + "integrity": "sha512-RNEqWOyhhUQYN9V1GfHwu9AR/g+NTciH6Z5u3/no6X3/w+04J2lVDL+svFQVXgXrEGBMG2puMVN3gq2SNGuTGw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/smob": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.2.tgz", + "integrity": "sha512-RQsvleCbF8cVHEv+xuDGaA4pOizFqJ0GgjtMSRo6oP8pnN7WsigHgVGey6aILRBKv4W2YOMHLqbKdnB6hpB9fw==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/source-map": { "version": "0.5.7", "license": "BSD-3-Clause", @@ -2055,12 +5917,150 @@ }, "node_modules/source-map-js": { "version": "1.2.1", - "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-object": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", + "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", + "license": "BSD-2-Clause", + "dependencies": { + "get-own-enumerable-property-symbols": "^3.0.0", + "is-obj": "^1.0.1", + "is-regexp": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", + "integrity": "sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/stylis": { "version": "4.2.0", "license": "MIT" @@ -2075,9 +6075,53 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tempy": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-0.6.0.tgz", + "integrity": "sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw==", + "license": "MIT", + "dependencies": { + "is-stream": "^2.0.0", + "temp-dir": "^2.0.0", + "type-fest": "^0.16.0", + "unique-string": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.50.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.50.0.tgz", + "integrity": "sha512-CN9BVxWhgS/hRxtUMjtC2uRWSTcSfQFHMDWma6sKKfIivCD91sM+FOPfvwoaRMqCSrUpe1nv3jDamd9eEQ4y+w==", + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", - "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -2094,10 +6138,95 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, "license": "0BSD", "optional": true }, + "node_modules/type-fest": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.16.0.tgz", + "integrity": "sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.8.tgz", + "integrity": "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==", + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.9", + "for-each": "^0.3.5", + "gopd": "^1.2.0", + "is-typed-array": "^1.1.15", + "possible-typed-array-names": "^1.1.0", + "reflect.getprototypeof": "^1.0.10" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "6.0.3", "dev": true, @@ -2110,14 +6239,132 @@ "node": ">=14.17" } }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici-types": { "version": "7.18.2", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "license": "MIT", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/upath": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", + "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", + "license": "MIT", + "engines": { + "node": ">=4", + "yarn": "*" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/vite": { "version": "8.1.5", - "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", @@ -2190,6 +6437,354 @@ "optional": true } } + }, + "node_modules/vite-plugin-pwa": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.3.0.tgz", + "integrity": "sha512-c5kMgN+ITrOtHXp8PAtk2uOIEea6XjP/unCGxOWWBzQ6qa65qj/awHg0wf+QF9E/2u9vh86LqxPwzEPNbM2r5A==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.6", + "pretty-bytes": "^6.1.1", + "tinyglobby": "^0.2.10", + "workbox-build": "^7.4.1", + "workbox-window": "^7.4.1" + }, + "engines": { + "node": ">=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vite-pwa/assets-generator": "^1.0.0", + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "workbox-build": "^7.4.1", + "workbox-window": "^7.4.1" + }, + "peerDependenciesMeta": { + "@vite-pwa/assets-generator": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/workbox-background-sync": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.1.tgz", + "integrity": "sha512-HhT7KE8tOWDm02wRNshXUnUPofMlhenF2DBdUnDPOubhizzPeItkYTmAB6td1Z2cjYPa98vzEiPLEuzn5hN66g==", + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-broadcast-update": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.1.tgz", + "integrity": "sha512-uAlgslKLvbQY+suirIdnBCSYrcgBhjp81Nj4l1lj/Jmj0MJO2CJERnCJjT0GFVwmReV0N+zs78K6gqd5gr9/+A==", + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-build": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.1.tgz", + "integrity": "sha512-SDhxIvEAde9Gy/5w4Yo1Jh/M49Z0qE3q0oteyE8zGq0DScxFqVBcCtIXFuLtmtxRQZCMbf0prco4VyEu3KBQuw==", + "license": "MIT", + "dependencies": { + "@apideck/better-ajv-errors": "^0.3.1", + "@babel/core": "^7.24.4", + "@babel/preset-env": "^7.11.0", + "@babel/runtime": "^7.11.2", + "@rollup/plugin-babel": "^6.1.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@rollup/plugin-replace": "^6.0.3", + "@rollup/plugin-terser": "^1.0.0", + "@trickfilm400/rollup-plugin-off-main-thread": "^3.0.0-pre1", + "ajv": "^8.6.0", + "common-tags": "^1.8.0", + "eta": "^4.5.1", + "fast-json-stable-stringify": "^2.1.0", + "fs-extra": "^9.0.1", + "glob": "^11.0.1", + "pretty-bytes": "^5.3.0", + "rollup": "^4.53.3", + "source-map": "^0.8.0-beta.0", + "stringify-object": "^3.3.0", + "strip-comments": "^2.0.1", + "tempy": "^0.6.0", + "upath": "^1.2.0", + "workbox-background-sync": "7.4.1", + "workbox-broadcast-update": "7.4.1", + "workbox-cacheable-response": "7.4.1", + "workbox-core": "7.4.1", + "workbox-expiration": "7.4.1", + "workbox-google-analytics": "7.4.1", + "workbox-navigation-preload": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-range-requests": "7.4.1", + "workbox-recipes": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1", + "workbox-streams": "7.4.1", + "workbox-sw": "7.4.1", + "workbox-window": "7.4.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/workbox-build/node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/workbox-build/node_modules/source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0.tgz", + "integrity": "sha512-d8EqvL+k/SOXCreS/SUzg2ciyHqBBLcN/yuRjFsbvVhHTE2pgei7oAhmPM7kWFbkX6OSMQfUq4KbkF3au9lhYQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/workbox-cacheable-response": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.1.tgz", + "integrity": "sha512-8xaFoJdDc2OjrlbbL3gEeBO1WKcMwRqwLRupgqahYXu75yXajPLuwrbXMrIGZuWYXrQwk0xDjOxZ/ujCy/oJYw==", + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-core": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.1.tgz", + "integrity": "sha512-DT+vu46eh/2vRsSHTY4Xmc32Z1rr9PRlQUXr1Dx30ZuXRWwOsvZgGgcwxcasubQLQmbTNYZjv44LkBAQ4tT5tQ==", + "license": "MIT" + }, + "node_modules/workbox-expiration": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.1.tgz", + "integrity": "sha512-lRKUF7b+OGbeXkQk1s6MHXOa3d7Xxf7Of31W6c6hCfipfIyrtdWZ89stq21AHZMaoG7VNFoHply4Ox+rU31TWg==", + "license": "MIT", + "dependencies": { + "idb": "^7.0.1", + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-google-analytics": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.1.tgz", + "integrity": "sha512-Mks1JwLEt++ZAkF6sS1OpSh9RtAMIsiDgRpK+codiHGIPXeaUOgi4cPc3GFadUl8V5QPeypEk8Oxgl3HlwVzHw==", + "license": "MIT", + "dependencies": { + "workbox-background-sync": "7.4.1", + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-navigation-preload": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.1.tgz", + "integrity": "sha512-C4KVsjPcYKJOhr631AxR9XoG2rLF3QiTk5aMv36MXOjtWvm8axwNFAtKUPGsWUwLXXAMgYM1En7fsvndaXeXRQ==", + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-precaching": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.1.tgz", + "integrity": "sha512-cdr/9qByww7yzEp7zg/qI4ukUrrNjQLgN+ONQRpjy/VqGQXwkgHwr00KksGJK8v0VifwDXBb8a4cWNZH71jn3Q==", + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-range-requests": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.1.tgz", + "integrity": "sha512-7i2oxAUE82gHdAJBCAQ04JzNOdRPqzuOzGfoUyJpFSmeqBNYGPrAH8GPoPjUQTfp+NycwrD2H68VtuF8qxv0vQ==", + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-recipes": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.1.tgz", + "integrity": "sha512-gnbVfmV4/TtmQaM4x9AtuXhcdstJsep3XMVeztOrQVPT+R6+6DeBjGTCQ7fFCXm+4GEHUA5VEBTyi5+4gWGeog==", + "license": "MIT", + "dependencies": { + "workbox-cacheable-response": "7.4.1", + "workbox-core": "7.4.1", + "workbox-expiration": "7.4.1", + "workbox-precaching": "7.4.1", + "workbox-routing": "7.4.1", + "workbox-strategies": "7.4.1" + } + }, + "node_modules/workbox-routing": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.1.tgz", + "integrity": "sha512-yubJGErZOusuidAenaL5ypfhQOa7urxP/f8E0ws7FPb4039RiWXUWBAyUkmUoOL/BcQGen3h0J8872d51IYxtA==", + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-strategies": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.1.tgz", + "integrity": "sha512-GZxpaw9NbmOelj7667uZ2kpk5BFpOGbO4X0qjwh5ls8XQ8C+Lha5LQchTiUzsTFSS+NlUpftYAyOVXvQUrcqOQ==", + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1" + } + }, + "node_modules/workbox-streams": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.1.tgz", + "integrity": "sha512-HWWtraKUbJknd9kgqGcpQ3G114HOPYvqs8HaJMDs2ebLNAimDkVDaWfAXE6Ybl+m8U6KsCE6pWyLYuigWmnAXw==", + "license": "MIT", + "dependencies": { + "workbox-core": "7.4.1", + "workbox-routing": "7.4.1" + } + }, + "node_modules/workbox-sw": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.1.tgz", + "integrity": "sha512-fez5f2DUlDJWTFYkCWQpY10N8gtztd849NswCbVFk0QlcSM4HT5A8x4g4ii650yem4I8tHY0R7JZahwp3ltIPw==", + "license": "MIT" + }, + "node_modules/workbox-window": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.1.tgz", + "integrity": "sha512-notZDH2u8VXaqyuD7xaqIfEFi6SRM4SUSd7ewe9PDsVqADuepxX2ZMY3uvuZGxzY5ZOsGC/vD3A/3smFtJt4/A==", + "license": "MIT", + "dependencies": { + "@types/trusted-types": "^2.0.2", + "workbox-core": "7.4.1" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" } } } diff --git a/package.json b/package.json index bd022e6..7938daa 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", + "build:customer": "tsc -b && vite build", "lint": "oxlint", "preview": "vite preview" }, @@ -16,7 +17,10 @@ "@mui/icons-material": "^9.2.0", "@mui/material": "^9.2.0", "react": "^19.2.7", - "react-dom": "^19.2.7" + "react-dom": "^19.2.7", + "react-router-dom": "^7.18.2", + "vite-plugin-pwa": "^1.3.0", + "workbox-window": "^7.4.1" }, "devDependencies": { "@types/node": "^24.13.2", diff --git a/packages/ui/package.json b/packages/ui/package.json new file mode 100644 index 0000000..298075f --- /dev/null +++ b/packages/ui/package.json @@ -0,0 +1,18 @@ +{ + "name": "@restroai/ui", + "private": true, + "version": "0.1.0", + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "peerDependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@mui/material": "^9.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + } +} diff --git a/packages/ui/src/StatusBadge.tsx b/packages/ui/src/StatusBadge.tsx new file mode 100644 index 0000000..1d1b855 --- /dev/null +++ b/packages/ui/src/StatusBadge.tsx @@ -0,0 +1,117 @@ +import React from 'react'; +import { Chip, Box, Typography } from '@mui/material'; + +export type DietaryType = 'veg' | 'non-veg' | 'jain' | 'vegan'; +export type OrderStatusLabel = 'pending' | 'preparing' | 'ready' | 'served'; +export type OrderTypeLabel = 'Dine-In' | 'Takeaway' | 'Delivery'; + +interface StatusBadgeProps { + status?: OrderStatusLabel; + type?: OrderTypeLabel; + dietary?: DietaryType; + size?: 'small' | 'medium'; +} + +/** Veg / non-veg / status / order-type badges shared by PWA + dashboard. */ +export const StatusBadge: React.FC = ({ + status, + type, + dietary, + size = 'small', +}) => { + if (dietary) { + const config = { + veg: { label: 'VEG', color: '#2e7d32', bg: '#e8f5e9', border: '#2e7d32' }, + 'non-veg': { label: 'NON-VEG', color: '#c62828', bg: '#ffebee', border: '#c62828' }, + jain: { label: 'JAIN', color: '#ef6c00', bg: '#fff3e0', border: '#ef6c00' }, + vegan: { label: 'VEGAN', color: '#1565c0', bg: '#e3f2fd', border: '#1565c0' }, + }[dietary]; + + return ( + + + + {config.label} + + + ); + } + + if (status) { + const statusMap = { + pending: { label: 'PENDING', color: 'error' as const, bg: '#ffdad6' }, + preparing: { label: 'PREPARING', color: 'warning' as const, bg: '#ffddba' }, + ready: { label: 'READY', color: 'success' as const, bg: '#a3f69c' }, + served: { label: 'SERVED', color: 'default' as const, bg: '#eeeeee' }, + }[status]; + + return ( + + ); + } + + if (type) { + const typeMap = { + 'Dine-In': { label: 'DINE-IN', color: '#ac2d00', bg: '#ffdbd1' }, + Takeaway: { label: 'TAKEAWAY', color: '#546067', bg: '#d7e4ec' }, + Delivery: { label: 'DELIVERY', color: '#845000', bg: '#ffddba' }, + }[type]; + + return ( + + ); + } + + return null; +}; + +/** Alias for dietary-only usage. */ +export const DietaryBadge: React.FC<{ dietary: DietaryType; size?: 'small' | 'medium' }> = ({ + dietary, + size, +}) => ; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts new file mode 100644 index 0000000..5af3295 --- /dev/null +++ b/packages/ui/src/index.ts @@ -0,0 +1,3 @@ +export { theme } from './theme'; +export { StatusBadge, DietaryBadge } from './StatusBadge'; +export type { DietaryType, OrderStatusLabel, OrderTypeLabel } from './StatusBadge'; diff --git a/packages/ui/src/theme.ts b/packages/ui/src/theme.ts new file mode 100644 index 0000000..c71309b --- /dev/null +++ b/packages/ui/src/theme.ts @@ -0,0 +1,146 @@ +import { createTheme } from '@mui/material/styles'; + +/** Shared RestroAI MUI theme (customer PWA + staff dashboard). */ +export const theme = createTheme({ + palette: { + mode: 'light', + primary: { + main: '#ac2d00', + light: '#ffb5a0', + dark: '#872100', + contrastText: '#ffffff', + }, + secondary: { + main: '#546067', + light: '#818e95', + dark: '#2a363d', + contrastText: '#ffffff', + }, + background: { + default: '#f8f9fa', + paper: '#ffffff', + }, + error: { + main: '#ba1a1a', + light: '#ffdad6', + dark: '#93000a', + }, + warning: { + main: '#845000', + light: '#ffddba', + dark: '#2b1700', + }, + success: { + main: '#11651d', + light: '#a3f69c', + dark: '#003915', + }, + info: { + main: '#00a6e0', + light: '#c4e7ff', + dark: '#00374d', + }, + text: { + primary: '#1a1c1c', + secondary: '#5b4139', + }, + divider: '#e4beb4', + }, + typography: { + fontFamily: '"Inter", "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + h1: { + fontFamily: '"Plus Jakarta Sans", "Inter", sans-serif', + fontWeight: 800, + letterSpacing: '-0.02em', + }, + h2: { + fontFamily: '"Plus Jakarta Sans", "Inter", sans-serif', + fontWeight: 700, + letterSpacing: '-0.01em', + }, + h3: { + fontFamily: '"Plus Jakarta Sans", "Inter", sans-serif', + fontWeight: 700, + }, + h4: { + fontFamily: '"Plus Jakarta Sans", "Inter", sans-serif', + fontWeight: 600, + }, + h5: { + fontFamily: '"Inter", sans-serif', + fontWeight: 600, + }, + h6: { + fontFamily: '"Inter", sans-serif', + fontWeight: 600, + }, + subtitle1: { + fontFamily: '"Inter", sans-serif', + fontWeight: 600, + }, + body1: { + fontFamily: '"Inter", sans-serif', + lineHeight: 1.5, + }, + body2: { + fontFamily: '"Inter", sans-serif', + lineHeight: 1.43, + }, + button: { + fontFamily: '"Inter", sans-serif', + fontWeight: 600, + textTransform: 'none', + }, + caption: { + fontFamily: '"JetBrains Mono", monospace', + fontWeight: 500, + }, + }, + shape: { + borderRadius: 8, + }, + components: { + MuiButton: { + styleOverrides: { + root: { + borderRadius: 8, + padding: '8px 16px', + boxShadow: 'none', + '&:hover': { + boxShadow: '0px 2px 8px rgba(172, 45, 0, 0.25)', + }, + }, + contained: { + background: 'linear-gradient(135deg, #ac2d00 0%, #d53e0b 100%)', + }, + }, + }, + MuiCard: { + styleOverrides: { + root: { + borderRadius: 12, + boxShadow: '0px 2px 12px rgba(0, 0, 0, 0.05)', + border: '1px solid rgba(228, 190, 180, 0.4)', + }, + }, + }, + MuiChip: { + styleOverrides: { + root: { + fontWeight: 600, + borderRadius: 6, + }, + }, + }, + MuiAppBar: { + styleOverrides: { + root: { + backgroundColor: '#ffffff', + color: '#1a1c1c', + boxShadow: '0px 1px 10px rgba(0,0,0,0.05)', + borderBottom: '1px solid #e2e2e2', + }, + }, + }, + }, +}); diff --git a/src/App.tsx b/src/App.tsx index f48e0af..4f4fc70 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,333 +1,2 @@ -import React, { useState, useEffect } from 'react'; -import { ThemeProvider, CssBaseline, Box, Alert } from '@mui/material'; -import { theme } from './theme/theme'; -import { TableLandingView } from './components/customer/TableLandingView'; -import { MenuBrowseView } from './components/customer/MenuBrowseView'; -import { CustomizationModal } from './components/customer/CustomizationModal'; -import { CartReviewView } from './components/customer/CartReviewView'; -import { OrderStatusView } from './components/customer/OrderStatusView'; -import { BillPaymentView } from './components/customer/BillPaymentView'; -import { VoiceAssistantModal } from './components/customer/VoiceAssistantModal'; -import { StaffLoginModal } from './components/admin/StaffLoginModal'; -import { AdminDesktopShell } from './components/admin/AdminDesktopShell'; -import type { MenuItem } from './data/menuData'; -import type { KDSOrder, OrderStatus } from './types'; -import { api, getBackendMenuId, getFrontendMenuItem, RestroWebSocket, subscribeToWsEvents, isDemoMode, setDemoModeChangeCallback } from './services/api'; - -type CustomerView = 'landing' | 'menu' | 'cart' | 'status' | 'bill' | 'voice'; - -const getTableNumFromUrl = (): number => { - const params = new URLSearchParams(window.location.search); - const tableParam = params.get('table'); - if (tableParam) { - const parsed = parseInt(tableParam); - if (!isNaN(parsed)) return parsed; - } - - const path = window.location.pathname; - const match = path.match(/\/scan\/(\d+)/); - if (match) { - return parseInt(match[1]); - } - - return 12; -}; - -export const App: React.FC = () => { - const [appMode, setAppMode] = useState<'customer' | 'staff'>('customer'); - const [customerView, setCustomerView] = useState('landing'); - const [staffRole, setStaffRole] = useState<'kitchen' | 'manager'>('kitchen'); - const [staffLoginOpen, setStaffLoginOpen] = useState(false); - const [voiceModalOpen, setVoiceModalOpen] = useState(false); - const [demoActive, setDemoActive] = useState(isDemoMode); - - const [cart, setCart] = useState<{ [itemId: string]: number }>({}); - const [customizingItem, setCustomizingItem] = useState(null); - const [orders, setOrders] = useState([]); - - // Monitor demo mode status - useEffect(() => { - setDemoActive(isDemoMode); - setDemoModeChangeCallback((demo) => { - setDemoActive(demo); - }); - }, []); - - // Fetch KDS board when staff logged in - const loadKDS = async () => { - try { - // Backend returns KdsBoardResponse: { restaurant_id, orders: KdsBoardOrder[] } - const data = await api.getKDSBoard(1); // 1 = restaurant ID - const ordersArray = Array.isArray(data) ? data : (data?.orders || []); - - // Map API Order model to KDSOrder type if format differs - const mappedOrders: KDSOrder[] = ordersArray.map((ord: any) => { - // Map KDS status to Order status - let overallStatus: OrderStatus = 'pending'; - if (ord.status === 'served') overallStatus = 'served'; - else if (ord.status === 'ready') overallStatus = 'ready'; - else if (ord.status === 'in_preparation' || ord.status === 'preparing') overallStatus = 'preparing'; - - return { - id: String(ord.id || ord.order_id), - ticketNumber: ord.ticketNumber || String(ord.id || ord.order_id).substring(0, 4), - tableNumber: ord.tableNumber || (ord.table_number ? String(ord.table_number) : 'Takeaway'), - orderType: ord.orderType || (ord.channel === 'touch' ? 'Dine-In' : 'Dine-In'), - status: overallStatus, - createdAt: ord.createdAt || ord.placed_at || new Date().toISOString(), - timeElapsedMinutes: ord.timeElapsedMinutes || 0, - priority: ord.priority || 'normal', - serverName: ord.serverName || 'System', - totalAmount: ord.totalAmount || Number(ord.subtotal || 0), - items: ord.items?.map((it: any) => { - const original = getFrontendMenuItem(it.menu_item_id); - return { - id: String(it.id), - name: it.name || original?.name || `Dish ${it.menu_item_id}`, - quantity: it.quantity, - price: it.price || (it.unit_price ? Number(it.unit_price) : (original?.price || 150)), - completed: it.kds_status === 'ready' || it.kds_status === 'served', - dietary: 'veg' - }; - }) || [] - }; - }); - - setOrders(mappedOrders); - } catch (err) { - console.error('Failed to reload KDS board', err); - } - }; - - useEffect(() => { - if (appMode === 'staff') { - loadKDS(); - - // Setup WebSocket connection - const ws = new RestroWebSocket('kds', '1'); - ws.connect(); - - // Subscribe to updates - const unsubscribe = subscribeToWsEvents((event) => { - if (event.type === 'kds.item_updated') { - loadKDS(); - } - }); - - return () => { - ws.close(); - unsubscribe(); - }; - } - }, [appMode]); - - const handleUpdateCart = (itemId: string, quantity: number) => { - setCart((prev) => { - const copy = { ...prev }; - if (quantity <= 0) { - delete copy[itemId]; - } else { - copy[itemId] = quantity; - } - return copy; - }); - }; - - const handleCustomizationConfirm = (item: MenuItem, quantity: number, _notes: string) => { - handleUpdateCart(item.id, (cart[item.id] || 0) + quantity); - }; - - const handleStartOrdering = async (language: string) => { - try { - const tableId = getTableNumFromUrl(); - await api.startSession(tableId, language === 'english' ? 'en' : 'hi'); - setCustomerView('menu'); - } catch (err) { - console.warn('Could not initialize session, entering offline menu browse', err); - setCustomerView('menu'); - } - }; - - const handlePlaceOrder = async (_orderNotes: string) => { - try { - const items = Object.entries(cart).map(([id, qty]) => ({ - menu_item_id: getBackendMenuId(id), - quantity: qty, - customization_notes: [] - })); - - await api.placeOrder(items); - setCart({}); - setCustomerView('status'); - } catch (err) { - alert('Failed to place order: ' + err); - } - }; - - const handleStatusChange = async (orderId: string, newStatus: OrderStatus) => { - try { - const statusMap: Record = { - pending: 'queued', - preparing: 'in_prep', - ready: 'ready', - served: 'served' - }; - const kdsStatus = statusMap[newStatus]; - - const order = orders.find((o) => o.id === orderId); - if (order) { - for (const item of order.items) { - await api.updateOrderItemStatus(item.id, kdsStatus); - } - } - loadKDS(); - } catch (err) { - console.error('Failed to change status', err); - } - }; - - const handleToggleItem = async (orderId: string, itemId: string) => { - try { - const order = orders.find((o) => o.id === orderId); - const item = order?.items.find((i) => i.id === itemId); - if (item) { - const nextStatus = item.completed ? 'in_prep' : 'ready'; - await api.updateOrderItemStatus(itemId, nextStatus); - loadKDS(); - } - } catch (err) { - console.error('Failed to toggle item', err); - } - }; - - const handleAddSampleOrder = async () => { - // Add a sample order via API or locally - try { - const items = [ - { menu_item_id: 9, quantity: 2 }, // Butter chicken - { menu_item_id: 11, quantity: 4 } // Garlic naan - ]; - await api.placeOrder(items); - loadKDS(); - } catch (e) { - console.error('Failed to add sample order', e); - } - }; - - const handleAddOrderFromVoice = async (_transcript: string) => { - try { - const items = [ - { menu_item_id: 9, quantity: 2, customization_notes: ['Via Voice assistant'] }, - { menu_item_id: 11, quantity: 3 } - ]; - await api.placeOrder(items); - loadKDS(); - } catch (e) { - console.error('Failed to place voice order', e); - } - }; - - const handleStaffLoginSuccess = (role: 'kitchen' | 'manager') => { - setStaffRole(role); - setAppMode('staff'); - }; - - return ( - - - - {demoActive && ( - - Running in Demo Mode (Mock Backend). Start restroai-backend locally to connect to live DB. - - )} - - {appMode === 'staff' ? ( - setAppMode('customer')} - /> - ) : ( - - {customerView === 'landing' && ( - setStaffLoginOpen(true)} - onOpenVoice={() => setVoiceModalOpen(true)} - /> - )} - - {customerView === 'menu' && ( - setCustomizingItem(item)} - onOpenVoice={() => setVoiceModalOpen(true)} - onNavigate={(v) => setCustomerView(v as CustomerView)} - /> - )} - - {customerView === 'cart' && ( - setCustomerView(v as CustomerView)} - /> - )} - - {customerView === 'status' && ( - setCustomerView(v as CustomerView)} /> - )} - - {customerView === 'bill' && ( - setCustomerView(v as CustomerView)} /> - )} - - {/* Item Customization Dialog */} - setCustomizingItem(null)} - onConfirm={handleCustomizationConfirm} - /> - - {/* AI Voice Assistant Modal (bottom sheet) */} - setVoiceModalOpen(false)} - onAddToCart={handleUpdateCart} - /> - - )} - - {/* Staff Login Modal */} - setStaffLoginOpen(false)} - onLoginSuccess={handleStaffLoginSuccess} - /> - - ); -}; - -export default App; +/** @deprecated Use CustomerApp from src/apps/customer — kept for tooling that imports ./App */ +export { CustomerApp as default, CustomerApp as App } from './apps/customer/App'; diff --git a/src/apps/customer/App.tsx b/src/apps/customer/App.tsx new file mode 100644 index 0000000..e3b00f2 --- /dev/null +++ b/src/apps/customer/App.tsx @@ -0,0 +1,441 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { ThemeProvider, CssBaseline, Box, Alert } from '@mui/material'; +import { theme } from '@restroai/ui'; +import { + TableLandingView, + type GuestTableOption, +} from '../../components/customer/TableLandingView'; +import { MenuBrowseView } from '../../components/customer/MenuBrowseView'; +import { CustomizationModal } from '../../components/customer/CustomizationModal'; +import { CartReviewView } from '../../components/customer/CartReviewView'; +import { OrderStatusView } from '../../components/customer/OrderStatusView'; +import { BillPaymentView } from '../../components/customer/BillPaymentView'; +import { VoiceAssistantModal } from '../../components/customer/VoiceAssistantModal'; +import type { MenuItem } from '../../data/menuData'; +import { MENU_ITEMS } from '../../data/menuData'; +import { + api, + flattenPublicMenu, + isDemoMode, + setDemoModeChangeCallback, +} from '../../services/api'; +import { LocaleProvider, useLocale } from '../../i18n/LocaleContext'; +import { + toSessionLanguage, + uiLangToLocale, +} from '../../i18n/messages'; +import { debugLog, maskToken } from '../../utils/debugLog'; + +type CustomerView = 'landing' | 'menu' | 'cart' | 'status' | 'bill' | 'voice'; + +type UrlTableBinding = + | { mode: 'qr'; tableId: number } + | { mode: 'none' }; + +/** QR `/scan/{id}` locks the table. Bare `?table=` is a hint only (walk-in can change). */ +const parseUrlTableBinding = (): UrlTableBinding => { + const path = window.location.pathname; + const match = path.match(/\/scan\/(\d+)/); + if (match) { + return { mode: 'qr', tableId: parseInt(match[1], 10) }; + } + return { mode: 'none' }; +}; + +const hintTableFromQuery = (): string | null => { + const params = new URLSearchParams(window.location.search); + const tableParam = params.get('table'); + if (tableParam && tableParam.trim()) return tableParam.trim(); + + // Tolerate malformed links like ?table-15 (hyphen instead of =) + const raw = window.location.search.replace(/^\?/, ''); + const malformed = raw.match(/(?:^|&)table-(\d+)(?:&|$)/i); + if (malformed) return malformed[1]; + return null; +}; + +const syncTableQuery = (tableNumber: string) => { + const url = new URL(window.location.href); + url.searchParams.set('table', tableNumber); + window.history.replaceState({}, '', url.toString()); +}; + +const CustomerAppInner: React.FC = () => { + const { setLocale, t } = useLocale(); + const [customerView, setCustomerView] = useState('landing'); + const [voiceModalOpen, setVoiceModalOpen] = useState(false); + const [demoActive, setDemoActive] = useState(isDemoMode); + const [cart, setCart] = useState<{ [itemId: string]: number }>({}); + const [cartNotes, setCartNotes] = useState<{ [itemId: string]: string }>({}); + const [lastPlacedOrder, setLastPlacedOrder] = useState< + { name: string; qty: number; price: number; notes?: string }[] + >(() => { + try { + const raw = sessionStorage.getItem('customer_last_order_items'); + return raw ? JSON.parse(raw) : []; + } catch { + return []; + } + }); + const [customizingItem, setCustomizingItem] = useState(null); + const [menuItems, setMenuItems] = useState(MENU_ITEMS); + const [offline, setOffline] = useState(!navigator.onLine); + + const [tableLocked, setTableLocked] = useState(false); + const [selectedTable, setSelectedTable] = useState(null); + const [availableTables, setAvailableTables] = useState([]); + const [tablesLoading, setTablesLoading] = useState(true); + const [tablesError, setTablesError] = useState(null); + + useEffect(() => { + setDemoActive(isDemoMode); + setDemoModeChangeCallback((demo) => setDemoActive(demo)); + }, []); + + useEffect(() => { + const on = () => setOffline(false); + const off = () => setOffline(true); + window.addEventListener('online', on); + window.addEventListener('offline', off); + return () => { + window.removeEventListener('online', on); + window.removeEventListener('offline', off); + }; + }, []); + + useEffect(() => { + let cancelled = false; + const loadTables = async () => { + setTablesLoading(true); + setTablesError(null); + const binding = parseUrlTableBinding(); + const hint = hintTableFromQuery(); + try { + // For QR lock we may need the assigned table even if currently occupied. + const availableOnly = binding.mode !== 'qr'; + const rows = await api.getPublicTables({ availableOnly }); + if (cancelled) return; + + const options: GuestTableOption[] = rows.map((r) => ({ + id: r.id, + table_number: r.table_number, + capacity: r.capacity, + status: r.status, + })); + setAvailableTables( + availableOnly ? options : options.filter((o) => o.status === 'available') + ); + + if (binding.mode === 'qr') { + setTableLocked(true); + const locked = + options.find((o) => o.id === binding.tableId) || + ({ + id: binding.tableId, + table_number: String(binding.tableId), + capacity: 0, + status: 'unknown', + } satisfies GuestTableOption); + setSelectedTable(locked); + syncTableQuery(locked.table_number); + } else { + setTableLocked(false); + const preferred = + (hint && + options.find( + (o) => + o.table_number === hint || + o.table_number === `T${hint}` || + String(o.id) === hint + )) || + options.find((o) => o.table_number === '12') || + options[0] || + null; + setSelectedTable(preferred); + if (preferred) syncTableQuery(preferred.table_number); + } + } catch (err) { + if (cancelled) return; + console.warn('Public tables fetch failed', err); + setTablesError('Could not load tables from server.'); + setTableLocked(binding.mode === 'qr'); + if (binding.mode === 'qr') { + setSelectedTable({ + id: binding.tableId, + table_number: String(binding.tableId), + capacity: 0, + status: 'unknown', + }); + } + } finally { + if (!cancelled) setTablesLoading(false); + } + }; + void loadTables(); + return () => { + cancelled = true; + }; + }, []); + + const handleSelectTable = useCallback((table: GuestTableOption) => { + if (parseUrlTableBinding().mode === 'qr') return; + setSelectedTable(table); + syncTableQuery(table.table_number); + // Changing table before session — clear any stale guest token. + sessionStorage.removeItem('customer_session_token'); + sessionStorage.removeItem('customer_session_id'); + sessionStorage.removeItem('customer_table_id'); + sessionStorage.removeItem('customer_table_number'); + sessionStorage.removeItem('customer_restaurant_id'); + }, []); + + const handleUpdateCart = (itemId: string, quantity: number) => { + setCart((prev) => { + const copy = { ...prev }; + if (quantity <= 0) delete copy[itemId]; + else copy[itemId] = quantity; + return copy; + }); + if (quantity <= 0) { + setCartNotes((prev) => { + const copy = { ...prev }; + delete copy[itemId]; + return copy; + }); + } + }; + + const handleCustomizationConfirm = (item: MenuItem, quantity: number, notes: string) => { + const id = String(item.id); + handleUpdateCart(id, (cart[id] || 0) + quantity); + if (notes?.trim()) { + setCartNotes((prev) => ({ + ...prev, + [id]: prev[id] ? `${prev[id]} · ${notes.trim()}` : notes.trim(), + })); + } + }; + + const handleStartOrdering = async (language: 'english' | 'hindi' | 'hinglish') => { + const locale = uiLangToLocale(language); + setLocale(locale); + if (!selectedTable) { + debugLog.warn('session', 'start ordering blocked — no table selected'); + alert(t('selectTable')); + return false; + } + debugLog.info('session', 'startSession begin', { + tableId: selectedTable.id, + tableNumber: selectedTable.table_number, + language: toSessionLanguage(locale), + }); + try { + const started = await api.startSession( + selectedTable.id, + toSessionLanguage(locale), + selectedTable.table_number, + ); + debugLog.info('session', 'startSession ok', { + sessionId: started?.session_id, + tableId: started?.table_id, + token: maskToken(started?.session_token), + }); + try { + const catalog = await api.getPublicMenu(); + const items = flattenPublicMenu(catalog); + if (items.length) setMenuItems(items); + debugLog.info('session', 'public menu loaded', { items: items.length }); + } catch (menuErr) { + console.warn('Public menu fetch failed; using local catalog fallback', menuErr); + setMenuItems(MENU_ITEMS); + } + setCustomerView('menu'); + return true; + } catch (err) { + debugLog.error('session', 'startSession failed', { + error: err instanceof Error ? err.message : String(err), + }); + console.warn('Could not initialize session', err); + alert( + err instanceof Error + ? err.message + : `Could not start session for table ${selectedTable.table_number}` + ); + return false; + } + }; + + const openVoiceAssistant = async () => { + const sessionTableId = sessionStorage.getItem('customer_table_id'); + const needsFreshSession = + !sessionStorage.getItem('customer_session_token') || + (selectedTable != null && sessionTableId !== String(selectedTable.id)); + debugLog.info('voice', 'openVoiceAssistant', { + needsFreshSession, + selectedTableId: selectedTable?.id, + sessionTableId, + hasToken: Boolean(sessionStorage.getItem('customer_session_token')), + }); + if (needsFreshSession) { + const ok = await handleStartOrdering('english'); + if (!ok || !sessionStorage.getItem('customer_session_token')) { + debugLog.error('voice', 'openVoiceAssistant aborted — session missing'); + return; + } + } + setVoiceModalOpen(true); + }; + + const handlePlaceOrder = async (orderNotes: string) => { + try { + const summary = Object.entries(cart).map(([id, qty]) => { + const item = menuItems.find((m) => String(m.id) === id); + const noteParts = [cartNotes[id], orderNotes].filter((n) => n && n.trim()); + return { + name: item?.name || `Item ${id}`, + qty, + price: item?.price || 0, + notes: noteParts.join(' · ') || undefined, + }; + }); + const items = Object.entries(cart).map(([id, qty]) => { + const notes: string[] = []; + if (cartNotes[id]?.trim()) notes.push(cartNotes[id].trim()); + if (orderNotes.trim()) notes.push(orderNotes.trim()); + return { + menu_item_id: Number(id), + quantity: qty, + customization_notes: notes, + }; + }); + await api.placeOrder(items); + setLastPlacedOrder(summary); + sessionStorage.setItem('customer_last_order_items', JSON.stringify(summary)); + setCart({}); + setCartNotes({}); + setCustomerView('status'); + } catch (err) { + alert('Failed to place order: ' + err); + } + }; + + const syncCartFromAi = useCallback((lines: { menu_item_id: number; quantity: number; notes?: string[] }[]) => { + const next: { [itemId: string]: number } = {}; + const nextNotes: { [itemId: string]: string } = {}; + for (const line of lines) { + if (line.quantity <= 0) continue; + next[String(line.menu_item_id)] = line.quantity; + if (line.notes?.length) nextNotes[String(line.menu_item_id)] = line.notes.join(', '); + } + setCart(next); + setCartNotes(nextNotes); + }, []); + + const openStaffDashboard = () => { + window.location.href = '/staff.html'; + }; + + return ( + <> + {demoActive && ( + + Running in Demo Mode (Mock Backend). Start restroai-backend locally to connect to live DB. + + )} + {offline && ( + + {t('offlineHint')} + + )} + + + {customerView === 'landing' && ( + void handleStartOrdering(lang)} + onOpenStaffLogin={openStaffDashboard} + onOpenVoice={() => void openVoiceAssistant()} + selectedTable={selectedTable} + tableLocked={tableLocked} + availableTables={availableTables} + tablesLoading={tablesLoading} + tablesError={tablesError} + onSelectTable={handleSelectTable} + /> + )} + + {customerView === 'menu' && ( + setCustomizingItem(item)} + onOpenVoice={() => void openVoiceAssistant()} + onNavigate={(v) => setCustomerView(v as CustomerView)} + /> + )} + + {customerView === 'cart' && ( + setCustomerView(v as CustomerView)} + /> + )} + + {customerView === 'status' && ( + setCustomerView(v as CustomerView)} + /> + )} + + {customerView === 'bill' && ( + setCustomerView(v as CustomerView)} /> + )} + + setCustomizingItem(null)} + onConfirm={handleCustomizationConfirm} + /> + + setVoiceModalOpen(false)} + onAddToCart={handleUpdateCart} + onSyncCart={syncCartFromAi} + /> + + + ); +}; + +export const CustomerApp: React.FC = () => ( + + + + + + +); + +export default CustomerApp; diff --git a/src/apps/customer/main.tsx b/src/apps/customer/main.tsx new file mode 100644 index 0000000..fb26b0f --- /dev/null +++ b/src/apps/customer/main.tsx @@ -0,0 +1,17 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import '../../index.css'; +import { CustomerApp } from './App'; + +// Service worker is registered in production builds only (vite-plugin-pwa). +if (import.meta.env.PROD) { + void import('virtual:pwa-register').then(({ registerSW }) => { + registerSW({ immediate: true }); + }); +} + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/src/apps/staff/App.tsx b/src/apps/staff/App.tsx new file mode 100644 index 0000000..6d8c32b --- /dev/null +++ b/src/apps/staff/App.tsx @@ -0,0 +1,379 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { ThemeProvider, CssBaseline, Box, Alert } from '@mui/material'; +import { theme } from '@restroai/ui'; +import { StaffLoginModal } from '../../components/admin/StaffLoginModal'; +import { AdminDesktopShell } from '../../components/admin/AdminDesktopShell'; +import type { StaffRole } from '../../components/admin/StaffLoginModal'; +import type { KDSOrder, OrderStatus } from '../../types'; +import type { MenuItem } from '../../data/menuData'; +import { MENU_ITEMS } from '../../data/menuData'; +import { + api, + clearTokens, + getAccessToken, + isDemoMode, + RestroWebSocket, + setDemoModeChangeCallback, + subscribeToWsEvents, + type WsConnectionState, +} from '../../services/api'; + +type RestaurantOption = { + id: number; + name: string; + is_home?: boolean; + is_active?: boolean; +}; + +const elapsedMinutes = (iso?: string) => { + if (!iso) return 0; + const ms = Date.now() - new Date(iso).getTime(); + return Math.max(0, Math.floor(ms / 60000)); +}; + +/** Column placement must follow item kds_status (buttons patch items, not order.status alone). */ +const deriveBoardStatus = ( + items: { kds_status?: string }[], + orderStatus?: string, +): OrderStatus => { + const statuses = items.map((i) => String(i.kds_status || 'queued')); + if (statuses.length) { + const allServed = statuses.every((s) => s === 'served'); + if (allServed) return 'served'; + const allReadyOrServed = statuses.every((s) => s === 'ready' || s === 'served'); + if (allReadyOrServed) return 'ready'; + if (statuses.some((s) => s === 'in_prep') || statuses.some((s) => s === 'ready' || s === 'served')) { + return 'preparing'; + } + return 'pending'; + } + if (orderStatus === 'served') return 'served'; + if (orderStatus === 'ready') return 'ready'; + if (orderStatus === 'in_preparation' || orderStatus === 'preparing') return 'preparing'; + return 'pending'; +}; + +const mapBoardOrders = (ordersArray: any[], menuItems: MenuItem[]): KDSOrder[] => + ordersArray.map((ord: any) => { + const rawItems = ord.items || []; + const overallStatus = deriveBoardStatus(rawItems, ord.status); + const placedAt = ord.createdAt || ord.placed_at || new Date().toISOString(); + const tableNumber = + ord.tableNumber || + (ord.table_number != null && ord.table_number !== '' ? String(ord.table_number) : undefined); + const channel = String(ord.channel || ''); + const orderType: KDSOrder['orderType'] = + ord.orderType || + (channel.includes('delivery') + ? 'Delivery' + : tableNumber + ? 'Dine-In' + : 'Takeaway'); + + return { + id: String(ord.id || ord.order_id), + ticketNumber: ord.ticketNumber || String(ord.id || ord.order_id), + tableNumber, + orderType, + status: overallStatus, + createdAt: placedAt, + timeElapsedMinutes: ord.timeElapsedMinutes ?? elapsedMinutes(placedAt), + priority: ord.priority || 'normal', + serverName: ord.serverName || undefined, + totalAmount: + ord.totalAmount || + Number(ord.subtotal || 0) || + rawItems.reduce( + (sum: number, it: any) => sum + Number(it.unit_price || it.price || 0) * Number(it.quantity || 0), + 0, + ), + items: rawItems.map((it: any) => { + const catalog = menuItems.find((m) => m.id === Number(it.menu_item_id)); + const name = it.name || catalog?.name || `Dish #${it.menu_item_id}`; + return { + id: String(it.id), + name, + quantity: it.quantity, + price: it.price || (it.unit_price != null ? Number(it.unit_price) : catalog?.price || 0), + completed: it.kds_status === 'ready' || it.kds_status === 'served', + dietary: (catalog?.dietary || + (/chicken|mutton|fish|prawn|egg|keema|kebab/i.test(name) ? 'non-veg' : 'veg')) as KDSOrder['items'][0]['dietary'], + }; + }), + }; + }); + +export const StaffApp: React.FC = () => { + const [authed, setAuthed] = useState(() => Boolean(getAccessToken())); + const [loginOpen, setLoginOpen] = useState(() => !getAccessToken()); + const [staffRole, setStaffRole] = useState('chef'); + const [restaurantId, setRestaurantId] = useState(1); + const [restaurants, setRestaurants] = useState([]); + const [orders, setOrders] = useState([]); + const [menuItems] = useState(MENU_ITEMS); + const [demoActive, setDemoActive] = useState(isDemoMode); + const [wsState, setWsState] = useState('idle'); + const [actionError, setActionError] = useState(null); + const [busyOrderId, setBusyOrderId] = useState(null); + + const hydrateMe = useCallback(async () => { + const me = await api.getMe(); + const role = String(me?.role || '').toLowerCase(); + if (role === 'manager' || role === 'chef' || role === 'waiter' || role === 'cashier') { + setStaffRole(role); + } + if (me.restaurant_id) setRestaurantId(me.restaurant_id); + if (Array.isArray(me.restaurants) && me.restaurants.length) { + setRestaurants(me.restaurants); + } else if (me.restaurant_id) { + setRestaurants([{ id: me.restaurant_id, name: `Restaurant #${me.restaurant_id}`, is_active: true }]); + } + }, []); + + useEffect(() => { + setDemoModeChangeCallback((demo) => setDemoActive(demo)); + if (authed) { + hydrateMe().catch(() => { + /* keep defaults */ + }); + } + }, [authed, hydrateMe]); + + const loadKDS = useCallback(async () => { + try { + const data = await api.getKDSBoard(restaurantId); + const ordersArray = Array.isArray(data) ? data : data?.orders || []; + setOrders(mapBoardOrders(ordersArray, menuItems)); + setActionError(null); + } catch (err) { + console.error('Failed to reload KDS board', err); + setActionError(err instanceof Error ? err.message : 'Failed to load KDS board'); + } + }, [menuItems, restaurantId]); + + useEffect(() => { + if (!authed) return; + + loadKDS(); + const ws = new RestroWebSocket('kds', String(restaurantId)); + ws.onStateChange = (state) => setWsState(state); + ws.onReconnected = () => { + loadKDS(); + }; + ws.connect(); + + const unsubscribe = subscribeToWsEvents((event) => { + if (event.type === 'kds.item_updated') { + loadKDS(); + } + }); + + const tick = window.setInterval(() => { + setOrders((prev) => + prev.map((o) => ({ ...o, timeElapsedMinutes: elapsedMinutes(o.createdAt) })), + ); + }, 30000); + + return () => { + ws.close(); + unsubscribe(); + window.clearInterval(tick); + }; + }, [authed, loadKDS, restaurantId]); + + const handleStatusChange = async (orderId: string, newStatus: OrderStatus) => { + const statusMap: Record = { + pending: 'queued', + preparing: 'in_prep', + ready: 'ready', + served: 'served', + }; + const kdsStatus = statusMap[newStatus]; + const order = orders.find((o) => o.id === orderId); + if (!order) return; + + setBusyOrderId(orderId); + // Optimistic move so the board feels live even before reload. + setOrders((prev) => + prev.map((o) => + o.id === orderId + ? { + ...o, + status: newStatus, + items: o.items.map((it) => ({ + ...it, + completed: newStatus === 'ready' || newStatus === 'served', + })), + } + : o, + ), + ); + + try { + await Promise.all(order.items.map((item) => api.updateOrderItemStatus(item.id, kdsStatus))); + await loadKDS(); + } catch (err) { + console.error('Failed to change status', err); + setActionError(err instanceof Error ? err.message : 'Failed to update order status'); + await loadKDS(); + } finally { + setBusyOrderId(null); + } + }; + + const handleToggleItem = async (orderId: string, itemId: string) => { + const order = orders.find((o) => o.id === orderId); + const item = order?.items.find((i) => i.id === itemId); + if (!item) return; + + const nextStatus = item.completed ? 'in_prep' : 'ready'; + setOrders((prev) => + prev.map((o) => { + if (o.id !== orderId) return o; + const items = o.items.map((it) => + it.id === itemId ? { ...it, completed: !item.completed } : it, + ); + return { ...o, items, status: deriveBoardStatus( + items.map((it) => ({ kds_status: it.completed ? 'ready' : 'in_prep' })), + o.status, + ) }; + }), + ); + + try { + await api.updateOrderItemStatus(itemId, nextStatus); + await loadKDS(); + } catch (err) { + console.error('Failed to toggle item', err); + setActionError(err instanceof Error ? err.message : 'Failed to update item'); + await loadKDS(); + } + }; + + const handleAddSampleOrder = async () => { + try { + setActionError(null); + await api.createKdsTestOrder(restaurantId); + await loadKDS(); + } catch (e) { + console.error('Failed to add sample order', e); + setActionError(e instanceof Error ? e.message : 'Failed to create test order'); + } + }; + + const handleAddOrderFromVoice = async (_transcript: string) => { + try { + await api.createKdsTestOrder(restaurantId); + await loadKDS(); + } catch (e) { + console.error('Failed to place voice order', e); + setActionError(e instanceof Error ? e.message : 'Failed to place voice order'); + } + }; + + const handleLoginSuccess = async (role: StaffRole) => { + setStaffRole(role); + setAuthed(true); + setLoginOpen(false); + try { + await hydrateMe(); + } catch { + /* ignore */ + } + }; + + const handleSwitchRestaurant = async (nextId: number) => { + if (nextId === restaurantId) return; + try { + await api.switchRestaurant(nextId); + setRestaurantId(nextId); + await hydrateMe(); + } catch (err) { + console.error('Failed to switch restaurant', err); + alert('Could not switch restaurant: ' + err); + } + }; + + const handleLogout = () => { + clearTokens(); + setAuthed(false); + setLoginOpen(true); + setWsState('closed'); + setRestaurants([]); + }; + + return ( + + + {demoActive && ( + + Running in Demo Mode (Mock Backend). + + )} + {actionError && ( + setActionError(null)} + sx={{ borderRadius: 0, fontWeight: 700 }} + > + {actionError} + + )} + + {authed ? ( + + ) : ( + + + RestroAI + Staff dashboard + + { + window.location.href = '/'; + }} + onLoginSuccess={handleLoginSuccess} + /> + + )} + + ); +}; + +export default StaffApp; diff --git a/src/apps/staff/main.tsx b/src/apps/staff/main.tsx new file mode 100644 index 0000000..c86cc3c --- /dev/null +++ b/src/apps/staff/main.tsx @@ -0,0 +1,11 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import '../../index.css'; +import './staff.css'; +import { StaffApp } from './App'; + +createRoot(document.getElementById('root')!).render( + + + , +); diff --git a/src/apps/staff/staff.css b/src/apps/staff/staff.css new file mode 100644 index 0000000..ab02783 --- /dev/null +++ b/src/apps/staff/staff.css @@ -0,0 +1,23 @@ +/* Staff dashboard: full-bleed shell (override Vite demo #root constraints) */ +html, +body, +#root { + width: 100%; + max-width: none; + margin: 0; + padding: 0; + text-align: left; + border: none; + min-height: 100vh; + min-height: 100svh; +} + +#root { + display: block; + box-sizing: border-box; +} + +body { + overflow-x: hidden; + background: #f4f5f7; +} diff --git a/src/components/admin/AdminDesktopShell.tsx b/src/components/admin/AdminDesktopShell.tsx index 1534c51..aca1b72 100644 --- a/src/components/admin/AdminDesktopShell.tsx +++ b/src/components/admin/AdminDesktopShell.tsx @@ -14,6 +14,11 @@ import { Button, Avatar, Divider, + Alert, + FormControl, + Select, + MenuItem, + InputLabel, } from '@mui/material'; import SoupKitchenIcon from '@mui/icons-material/SoupKitchen'; import BarChartIcon from '@mui/icons-material/BarChart'; @@ -28,6 +33,9 @@ import InventoryIcon from '@mui/icons-material/Inventory'; import LocalShippingIcon from '@mui/icons-material/LocalShipping'; import BadgeIcon from '@mui/icons-material/Badge'; import StarHalfIcon from '@mui/icons-material/StarHalf'; +import RestaurantMenuIcon from '@mui/icons-material/RestaurantMenu'; +import WifiOffIcon from '@mui/icons-material/WifiOff'; +import WifiIcon from '@mui/icons-material/Wifi'; import { KDSKanban } from '../kds/KDSKanban'; import { AnalyticsDashboard } from '../analytics/AnalyticsDashboard'; @@ -39,21 +47,30 @@ import { InventoryView } from './InventoryView'; import { SuppliersView } from './SuppliersView'; import { StaffShiftsView } from './StaffShiftsView'; import { FeedbackInboxView } from './FeedbackInboxView'; +import { MenuManagementView } from './MenuManagementView'; +import { LowStockNotifier } from './LowStockNotifier'; import type { KDSOrder, OrderStatus } from '../../types'; +import type { StaffRole } from './StaffLoginModal'; interface AdminDesktopShellProps { - role: 'kitchen' | 'manager'; + role: StaffRole; orders: KDSOrder[]; + restaurantId: number; + restaurants?: { id: number; name: string; is_home?: boolean; is_active?: boolean }[]; + onSwitchRestaurant?: (restaurantId: number) => void; onStatusChange: (id: string, newStatus: OrderStatus) => void; onToggleItem: (orderId: string, itemId: string) => void; onAddSampleOrder: () => void; onAddOrderFromVoice: (transcript: string) => void; onLogout: () => void; + wsState?: 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed'; + busyOrderId?: string | null; } type TabType = | 'kds' + | 'menu' | 'tables' | 'billing' | 'reservations' @@ -64,56 +81,78 @@ type TabType = | 'analytics' | 'voice'; +const defaultTabForRole = (role: StaffRole): TabType => { + if (role === 'manager') return 'analytics'; + if (role === 'cashier') return 'billing'; + if (role === 'waiter') return 'tables'; + return 'kds'; +}; + export const AdminDesktopShell: React.FC = ({ role, orders, + restaurantId, + restaurants = [], + onSwitchRestaurant, onStatusChange, onToggleItem, onAddSampleOrder, onAddOrderFromVoice, onLogout, + wsState = 'idle', + busyOrderId = null, }) => { - const [activeTab, setActiveTab] = useState( - role === 'manager' ? 'analytics' : 'kds' - ); + const [activeTab, setActiveTab] = useState(defaultTabForRole(role)); const pendingCount = orders.filter((o) => o.status === 'pending').length; - const drawerWidth = 260; + const drawerWidth = 232; - // Sidebar list configurations - const menuItems = [ - { id: 'kds', label: 'KDS Kanban Board', icon: , roles: ['kitchen', 'manager'] }, - { id: 'tables', label: 'Tables & Floor', icon: , roles: ['manager'] }, - { id: 'billing', label: 'Billing Desk', icon: , roles: ['manager'] }, - { id: 'reservations', label: 'Reservations', icon: , roles: ['manager'] }, - { id: 'inventory', label: 'Kitchen Stock & Recipes', icon: , roles: ['kitchen', 'manager'] }, - { id: 'suppliers', label: 'Suppliers & POs', icon: , roles: ['manager'] }, - { id: 'staff', label: 'Staff & Shifts', icon: , roles: ['manager'] }, - { id: 'feedback', label: 'Customer Feedback', icon: , roles: ['manager'] }, - { id: 'analytics', label: 'Business Intelligence', icon: , roles: ['manager'] }, - { id: 'voice', label: 'AI Voice Terminal', icon: , roles: ['kitchen', 'manager'] }, + // Sidebar list configurations — matches roadmap §2.4 + const menuItems: { id: TabType; label: string; icon: React.ReactNode; roles: StaffRole[] }[] = [ + { id: 'kds', label: 'KDS Kanban Board', icon: , roles: ['chef', 'waiter', 'manager'] }, + { id: 'menu', label: 'Menu Management', icon: , roles: ['manager'] }, + { id: 'tables', label: 'Tables & Floor', icon: , roles: ['waiter', 'manager'] }, + { id: 'billing', label: 'Billing Desk', icon: , roles: ['cashier', 'waiter', 'manager'] }, + { id: 'reservations', label: 'Reservations', icon: , roles: ['waiter', 'manager'] }, + { id: 'inventory', label: 'Kitchen Stock & Recipes', icon: , roles: ['chef', 'manager'] }, + { id: 'suppliers', label: 'Suppliers & POs', icon: , roles: ['manager'] }, + { id: 'staff', label: 'Staff & Shifts', icon: , roles: ['manager'] }, + { id: 'feedback', label: 'Customer Feedback', icon: , roles: ['manager'] }, + { id: 'analytics', label: 'Business Intelligence', icon: , roles: ['manager'] }, + { id: 'voice', label: 'AI Voice Terminal', icon: , roles: ['chef', 'waiter', 'manager'] }, ]; const filteredMenuItems = menuItems.filter((item) => item.roles.includes(role)); const getPageTitle = () => { switch (activeTab) { - case 'kds': return 'Kitchen Display System (KDS) Live Kanban'; - case 'tables': return 'Table Floor & Session Planner'; - case 'billing': return 'Cashier Billing Desk & Payments'; - case 'reservations': return 'Diner Reservations Book'; - case 'inventory': return 'Kitchen Raw Stocks & Recipes Linking'; - case 'suppliers': return 'Merchant Suppliers & Purchase Orders'; - case 'staff': return 'Employee Attendance & Shift Roster'; - case 'feedback': return 'Customer Feedback Rating Reviews'; - case 'analytics': return 'Executive Business Analytics & Trends'; - case 'voice': return 'Staff AI Voice Assistant Terminal'; - default: return 'Management Portal'; + case 'kds': return 'KDS Live Kanban'; + case 'menu': return 'Menu Management'; + case 'tables': return 'Tables & Floor'; + case 'billing': return 'Billing Desk'; + case 'reservations': return 'Reservations'; + case 'inventory': return 'Kitchen Stock & Recipes'; + case 'suppliers': return 'Suppliers & POs'; + case 'staff': return 'Staff & Shifts'; + case 'feedback': return 'Customer Feedback'; + case 'analytics': return 'Business Intelligence'; + case 'voice': return 'AI Voice Terminal'; + default: return 'Staff Portal'; } }; + const roleLabel = + role === 'manager' + ? 'Admin Manager' + : role === 'cashier' + ? 'Cashier' + : role === 'waiter' + ? 'Waiter' + : 'Chef'; + return ( - + + {/* Desktop Sidebar Navigation */} = ({ bgcolor: '#1a1c1c', color: '#ffffff', borderRight: '1px solid #2f3131', + display: 'flex', + flexDirection: 'column', + height: '100vh', }, }} > - + - + - - + + RestroAI - - DESKTOP STAFF PORTAL + + STAFF PORTAL - + {role === 'manager' ? 'M' : 'K'}} - label={role === 'manager' ? 'Role: Restaurant Manager' : 'Role: Kitchen Head Chef'} + avatar={ + + {role === 'manager' ? 'M' : role === 'cashier' ? 'C' : role === 'waiter' ? 'W' : 'K'} + + } + label={roleLabel} + size="small" sx={{ bgcolor: 'rgba(255,255,255,0.08)', color: '#ffffff', fontWeight: 700, width: '100%', justifyContent: 'flex-start', + mb: restaurants.length > 1 ? 1.25 : 0, }} /> + {restaurants.length > 1 && onSwitchRestaurant && ( + + + Location + + + + )} - + {filteredMenuItems.map((item) => ( - + setActiveTab(item.id as TabType)} + dense sx={{ borderRadius: '8px', + py: 0.75, '&.Mui-selected': { bgcolor: '#ac2d00', color: '#ffffff' }, '&.Mui-selected:hover': { bgcolor: '#872100' }, }} > - + {item.icon} + {item.label} } /> {item.id === 'kds' && pendingCount > 0 && ( - + )} ))} - + {/* Main Content Area */} - - - - + + {(wsState === 'reconnecting' || wsState === 'connecting') && ( + } + sx={{ borderRadius: 0, fontWeight: 700, py: 0.5 }} + > + Reconnecting to kitchen… board will refresh when live. + + )} + + + {getPageTitle()} - + : } + label={ + wsState === 'open' + ? 'Kitchen live' + : wsState === 'reconnecting' || wsState === 'connecting' + ? 'Reconnecting…' + : 'WS idle' + } + color={wsState === 'open' ? 'success' : 'warning'} size="small" sx={{ fontWeight: 800 }} /> - - - - Logged in as {role === 'manager' ? 'Admin Manager' : 'Chef Rahul S.'} + + + + {roleLabel} - + {activeTab === 'kds' && ( )} + {activeTab === 'menu' && } + {activeTab === 'tables' && } {activeTab === 'billing' && } diff --git a/src/components/admin/FeedbackInboxView.tsx b/src/components/admin/FeedbackInboxView.tsx index 5203afc..ec89b32 100644 --- a/src/components/admin/FeedbackInboxView.tsx +++ b/src/components/admin/FeedbackInboxView.tsx @@ -16,6 +16,7 @@ import { MenuItem, Button, Grid, + Chip, } from '@mui/material'; import StarIcon from '@mui/icons-material/Star'; import RefreshIcon from '@mui/icons-material/Refresh'; @@ -133,9 +134,30 @@ export const FeedbackInboxView: React.FC = () => { } secondary={ - - {fb.comment_text || '(No comment left by guest)'} - + + + {fb.comment_text || '(No comment left by guest)'} + + {(fb.sentiment_results || []).length > 0 && ( + + {fb.sentiment_results.map((row: any) => ( + + ))} + + )} + } /> diff --git a/src/components/admin/InventoryView.tsx b/src/components/admin/InventoryView.tsx index 273806e..345170c 100644 --- a/src/components/admin/InventoryView.tsx +++ b/src/components/admin/InventoryView.tsx @@ -36,10 +36,11 @@ import AddIcon from '@mui/icons-material/Add'; import RefreshIcon from '@mui/icons-material/Refresh'; import SaveIcon from '@mui/icons-material/Save'; import { api } from '../../services/api'; -import { MENU_ITEMS } from '../../data/menuData'; +import type { StaffMenuItem } from '../../services/api'; export const InventoryView: React.FC = () => { const [ingredients, setIngredients] = useState([]); + const [menuDishes, setMenuDishes] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -65,8 +66,16 @@ export const InventoryView: React.FC = () => { setLoading(true); setError(null); try { - const data = await api.getIngredients(); + const [data, dishes] = await Promise.all([ + api.getIngredients(), + api.listMenuItems().catch(() => []), + ]); setIngredients(Array.isArray(data) ? data : []); + const dishList = Array.isArray(dishes) ? dishes : []; + setMenuDishes(dishList); + if (dishList.length && !dishList.some((d) => d.id === selectedMenuId)) { + setSelectedMenuId(dishList[0].id); + } } catch (err: any) { setError(err.message || 'Failed to fetch ingredients'); } finally { @@ -271,9 +280,9 @@ export const InventoryView: React.FC = () => { label="Select Menu Dish" onChange={(e) => setSelectedMenuId(Number(e.target.value))} > - {MENU_ITEMS.map((item) => ( - - {item.name} (₹{item.price}) + {menuDishes.map((item) => ( + + {item.name} (₹{Number(item.price)}) ))} diff --git a/src/components/admin/LowStockNotifier.tsx b/src/components/admin/LowStockNotifier.tsx new file mode 100644 index 0000000..c8eb851 --- /dev/null +++ b/src/components/admin/LowStockNotifier.tsx @@ -0,0 +1,114 @@ +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { Alert, Button, Snackbar } from '@mui/material'; +import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive'; +import { api } from '../../services/api'; + +const POLL_MS = 5 * 60 * 1000; // every 5 minutes +const DISMISS_MS = 60 * 1000; + +type LowRow = { id: number; name: string; current_stock?: number; unit?: string }; + +function fingerprint(rows: LowRow[]): string { + return rows + .map((r) => `${r.id}:${r.current_stock ?? ''}`) + .sort() + .join('|'); +} + +/** + * Periodic low-stock toast for chef/manager while the staff portal is open. + * Also can push WhatsApp/notify when stock newly crosses into low. + */ +export const LowStockNotifier: React.FC<{ + enabled?: boolean; + autoNotify?: boolean; +}> = ({ enabled = true, autoNotify = false }) => { + const [open, setOpen] = useState(false); + const [message, setMessage] = useState(''); + const [count, setCount] = useState(0); + const lastFp = useRef(''); + const dismissedUntil = useRef(0); + + const check = useCallback(async () => { + if (!enabled) return; + try { + const rows = await api.getLowStockInventory(); + const list: LowRow[] = Array.isArray(rows) ? rows : rows?.items || []; + setCount(list.length); + if (!list.length) { + lastFp.current = ''; + return; + } + const fp = fingerprint(list); + const changed = fp !== lastFp.current; + lastFp.current = fp; + if (!changed && Date.now() < dismissedUntil.current) return; + + const names = list + .slice(0, 4) + .map((r) => r.name) + .join(', '); + const extra = list.length > 4 ? ` +${list.length - 4} more` : ''; + setMessage(`${list.length} low-stock item${list.length === 1 ? '' : 's'}: ${names}${extra}`); + setOpen(true); + + if (autoNotify && changed) { + try { + await api.notifyLowStock(); + } catch { + /* optional outbound notify */ + } + } + } catch { + /* silent — board can stay up without inventory */ + } + }, [autoNotify, enabled]); + + useEffect(() => { + if (!enabled) return; + void check(); + const id = window.setInterval(() => void check(), POLL_MS); + return () => window.clearInterval(id); + }, [check, enabled]); + + if (!enabled) return null; + + return ( + { + setOpen(false); + dismissedUntil.current = Date.now() + POLL_MS; + }} + > + } + sx={{ fontWeight: 700, alignItems: 'center' }} + action={ + + } + onClose={() => { + setOpen(false); + dismissedUntil.current = Date.now() + POLL_MS; + }} + > + {message || `${count} ingredients need reorder`} + + + ); +}; diff --git a/src/components/admin/MenuManagementView.tsx b/src/components/admin/MenuManagementView.tsx new file mode 100644 index 0000000..f8b8fee --- /dev/null +++ b/src/components/admin/MenuManagementView.tsx @@ -0,0 +1,373 @@ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { + Alert, + Box, + Button, + Chip, + CircularProgress, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControlLabel, + Grid, + IconButton, + List, + ListItemButton, + ListItemText, + Paper, + Switch, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TextField, + Typography, +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import { api, type StaffMenuCategory, type StaffMenuItem } from '../../services/api'; + +export const MenuManagementView: React.FC = () => { + const [categories, setCategories] = useState([]); + const [items, setItems] = useState([]); + const [selectedCategoryId, setSelectedCategoryId] = useState('all'); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [categoryDialogOpen, setCategoryDialogOpen] = useState(false); + const [categoryName, setCategoryName] = useState(''); + const [categoryOrder, setCategoryOrder] = useState(0); + + const [itemDialogOpen, setItemDialogOpen] = useState(false); + const [editingItem, setEditingItem] = useState(null); + const [itemName, setItemName] = useState(''); + const [itemPrice, setItemPrice] = useState('100'); + const [itemDescription, setItemDescription] = useState(''); + const [itemCategoryId, setItemCategoryId] = useState(''); + const [itemAvailable, setItemAvailable] = useState(true); + + const load = useCallback(async () => { + setLoading(true); + setError(null); + try { + const [cats, menuItems] = await Promise.all([ + api.listMenuCategories(), + api.listMenuItems(), + ]); + setCategories(Array.isArray(cats) ? cats : []); + setItems(Array.isArray(menuItems) ? menuItems : []); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Failed to load menu'); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + void load(); + }, [load]); + + const filteredItems = useMemo(() => { + if (selectedCategoryId === 'all') return items; + return items.filter((item) => item.category_id === selectedCategoryId); + }, [items, selectedCategoryId]); + + const openCreateItem = () => { + setEditingItem(null); + setItemName(''); + setItemPrice('100'); + setItemDescription(''); + setItemCategoryId(typeof selectedCategoryId === 'number' ? selectedCategoryId : categories[0]?.id || ''); + setItemAvailable(true); + setItemDialogOpen(true); + }; + + const openEditItem = (item: StaffMenuItem) => { + setEditingItem(item); + setItemName(item.name); + setItemPrice(String(item.price)); + setItemDescription(item.description || ''); + setItemCategoryId(item.category_id ?? ''); + setItemAvailable(item.is_available); + setItemDialogOpen(true); + }; + + const saveCategory = async () => { + try { + await api.createMenuCategory({ + name: categoryName.trim(), + display_order: categoryOrder, + }); + setCategoryDialogOpen(false); + setCategoryName(''); + await load(); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Failed to create category'); + } + }; + + const saveItem = async () => { + try { + const payload = { + name: itemName.trim(), + price: itemPrice, + description: itemDescription || null, + category_id: itemCategoryId === '' ? null : Number(itemCategoryId), + is_available: itemAvailable, + }; + if (editingItem) { + await api.updateMenuItem(editingItem.id, payload); + } else { + await api.createMenuItem(payload); + } + setItemDialogOpen(false); + await load(); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Failed to save item'); + } + }; + + const toggleAvailability = async (item: StaffMenuItem) => { + try { + await api.updateMenuItem(item.id, { is_available: !item.is_available }); + await load(); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Failed to update availability'); + } + }; + + const deleteItem = async (item: StaffMenuItem) => { + if (!window.confirm(`Delete "${item.name}"?`)) return; + try { + await api.deleteMenuItem(item.id); + await load(); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Failed to delete item'); + } + }; + + const deleteCategory = async (category: StaffMenuCategory) => { + if (!window.confirm(`Delete category "${category.name}"? Items keep existing but become uncategorized.`)) return; + try { + await api.deleteMenuCategory(category.id); + if (selectedCategoryId === category.id) setSelectedCategoryId('all'); + await load(); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'Failed to delete category'); + } + }; + + if (loading) { + return ( + + + + ); + } + + return ( + + + + Menu Catalog + + + + + + + + + {error && ( + setError(null)}> + {error} + + )} + + + + + + setSelectedCategoryId('all')} + > + + + {categories.map((category) => ( + setSelectedCategoryId(category.id)} + > + + { + event.stopPropagation(); + void deleteCategory(category); + }} + > + + + + ))} + + + + + + + + + + Name + Category + Price + Available + Actions + + + + {filteredItems.map((item) => ( + + + {item.name} + + {item.description || '—'} + + + + + + ₹{Number(item.price).toFixed(2)} + + void toggleAvailability(item)} + size="small" + /> + + + openEditItem(item)}> + + + void deleteItem(item)}> + + + + + ))} + {filteredItems.length === 0 && ( + + + + No menu items in this category yet. + + + + )} + +
+
+
+
+ + setCategoryDialogOpen(false)} fullWidth maxWidth="xs"> + New category + + setCategoryName(e.target.value)} + fullWidth + /> + setCategoryOrder(Number(e.target.value))} + fullWidth + /> + + + + + + + + setItemDialogOpen(false)} fullWidth maxWidth="sm"> + {editingItem ? 'Edit menu item' : 'New menu item'} + + setItemName(e.target.value)} fullWidth /> + setItemPrice(e.target.value)} + fullWidth + /> + setItemDescription(e.target.value)} + fullWidth + multiline + minRows={2} + /> + setItemCategoryId(e.target.value === '' ? '' : Number(e.target.value))} + fullWidth + slotProps={{ + select: { native: true }, + }} + > + + {categories.map((category) => ( + + ))} + + setItemAvailable(e.target.checked)} /> + } + label="Available" + /> + + + + + + +
+ ); +}; diff --git a/src/components/admin/StaffLoginModal.tsx b/src/components/admin/StaffLoginModal.tsx index 7e8b6ab..c044c78 100644 --- a/src/components/admin/StaffLoginModal.tsx +++ b/src/components/admin/StaffLoginModal.tsx @@ -13,15 +13,24 @@ import { CircularProgress, } from '@mui/material'; import LockIcon from '@mui/icons-material/Lock'; -import BadgeIcon from '@mui/icons-material/Badge'; import { api } from '../../services/api'; +export type StaffRole = 'chef' | 'waiter' | 'cashier' | 'manager'; + interface StaffLoginModalProps { open: boolean; onClose: () => void; - onLoginSuccess: (role: 'kitchen' | 'manager') => void; + onLoginSuccess: (role: StaffRole) => void; } +const normalizeRole = (role: string): StaffRole => { + const value = role.trim().toLowerCase(); + if (value === 'manager') return 'manager'; + if (value === 'waiter') return 'waiter'; + if (value === 'cashier') return 'cashier'; + return 'chef'; +}; + export const StaffLoginModal: React.FC = ({ open, onClose, @@ -43,10 +52,8 @@ export const StaffLoginModal: React.FC = ({ setError(''); try { await api.login(username, password); - // Determine role based on JWT claims or username - const role = username.includes('manager') ? 'manager' : 'kitchen'; - onLoginSuccess(role); - onClose(); + const me = await api.getMe(); + onLoginSuccess(normalizeRole(me.role)); } catch (err: any) { setError(err.message || 'Login failed. Please check your credentials.'); } finally { @@ -81,7 +88,7 @@ export const StaffLoginModal: React.FC = ({ Staff & Admin Portal Login
- Authorized Restaurant Personnel Only + Role comes from JWT via /auth/me @@ -97,70 +104,62 @@ export const StaffLoginModal: React.FC = ({ QUICK AUTOFILL DEMO LOGIN: - + } - label="Manager Account" + label="Manager" clickable - onClick={() => fillCredentials('manager@test.com', 'managerpass')} - sx={{ fontWeight: 700, flex: 1 }} + onClick={() => fillCredentials('manager@demo.restro', 'Manager@12345')} + sx={{ fontWeight: 700 }} /> } - label="Chef / Waiter Account" + label="Chef" clickable - onClick={() => fillCredentials('waiter@test.com', 'waiterpass')} - sx={{ fontWeight: 700, flex: 1 }} + onClick={() => fillCredentials('chef@demo.restro', 'Chef@12345')} + sx={{ fontWeight: 700 }} + /> + fillCredentials('waiter@demo.restro', 'Waiter@12345')} + sx={{ fontWeight: 700 }} + /> + fillCredentials('cashier@demo.restro', 'Cashier@12345')} + sx={{ fontWeight: 700 }} /> setUsername(e.target.value)} - sx={{ mb: 2 }} - disabled={loading} + autoComplete="username" /> - setPassword(e.target.value)} - sx={{ mb: 2.5 }} - disabled={loading} + autoComplete="current-password" /> - + + + + - - - - ); }; diff --git a/src/components/admin/StaffShiftsView.tsx b/src/components/admin/StaffShiftsView.tsx index 668f866..abcce12 100644 --- a/src/components/admin/StaffShiftsView.tsx +++ b/src/components/admin/StaffShiftsView.tsx @@ -41,7 +41,7 @@ export const StaffShiftsView: React.FC = () => { const [staffName, setStaffName] = useState(''); const [staffEmail, setStaffEmail] = useState(''); const [staffPassword, setStaffPassword] = useState(''); - const [staffRoleId, setStaffRoleId] = useState(2); // default waiter role_id + const [staffRoleId] = useState(2); // default waiter role_id const [staffRole, setStaffRole] = useState('waiter'); // display only const [roles, setRoles] = useState([]); // RoleRead[] from backend diff --git a/src/components/admin/SuppliersView.tsx b/src/components/admin/SuppliersView.tsx index b37e114..28c859e 100644 --- a/src/components/admin/SuppliersView.tsx +++ b/src/components/admin/SuppliersView.tsx @@ -127,7 +127,16 @@ export const SuppliersView: React.FC = () => { return; } // Backend schema: { ingredient_id, quantity, unit_price } - setPoItems(prev => [...prev, { ingredient_id: Number(addIngId), quantity: addIngQty, unit_price: addIngPrice }]); + setPoItems(prev => [ + ...prev, + { + ingredient_id: Number(addIngId), + quantity: addIngQty, + unit_price: addIngPrice, + qty: addIngQty, + price: addIngPrice, + }, + ]); setAddIngId(''); }; diff --git a/src/components/common/StatusBadge.tsx b/src/components/common/StatusBadge.tsx index edb4d60..afae3fa 100644 --- a/src/components/common/StatusBadge.tsx +++ b/src/components/common/StatusBadge.tsx @@ -1,102 +1,2 @@ -import React from 'react'; -import { Chip, Box, Typography } from '@mui/material'; -import type { OrderStatus, OrderType } from '../../types'; - -interface StatusBadgeProps { - status?: OrderStatus; - type?: OrderType; - dietary?: 'veg' | 'non-veg' | 'jain' | 'vegan'; - size?: 'small' | 'medium'; -} - -export const StatusBadge: React.FC = ({ status, type, dietary, size = 'small' }) => { - if (dietary) { - const config = { - veg: { label: 'VEG', color: '#2e7d32', bg: '#e8f5e9', border: '#2e7d32' }, - 'non-veg': { label: 'NON-VEG', color: '#c62828', bg: '#ffebee', border: '#c62828' }, - jain: { label: 'JAIN', color: '#ef6c00', bg: '#fff3e0', border: '#ef6c00' }, - vegan: { label: 'VEGAN', color: '#1565c0', bg: '#e3f2fd', border: '#1565c0' }, - }[dietary]; - - return ( - - - - {config.label} - - - ); - } - - if (status) { - const statusMap = { - pending: { label: 'PENDING', color: 'error', bg: '#ffdad6' }, - preparing: { label: 'PREPARING', color: 'warning', bg: '#ffddba' }, - ready: { label: 'READY', color: 'success', bg: '#a3f69c' }, - served: { label: 'SERVED', color: 'default', bg: '#eeeeee' }, - }[status]; - - return ( - - ); - } - - if (type) { - const typeMap = { - 'Dine-In': { label: 'DINE-IN', color: '#ac2d00', bg: '#ffdbd1' }, - Takeaway: { label: 'TAKEAWAY', color: '#546067', bg: '#d7e4ec' }, - Delivery: { label: 'DELIVERY', color: '#845000', bg: '#ffddba' }, - }[type]; - - return ( - - ); - } - - return null; -}; +/** Re-export shared badges from @restroai/ui. */ +export { StatusBadge, DietaryBadge } from '@restroai/ui'; diff --git a/src/components/customer/BillPaymentView.tsx b/src/components/customer/BillPaymentView.tsx index 774bed6..38a68e0 100644 --- a/src/components/customer/BillPaymentView.tsx +++ b/src/components/customer/BillPaymentView.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { Box, Container, @@ -9,81 +9,239 @@ import { Divider, Alert, Grid, + CircularProgress, } from '@mui/material'; import ReceiptLongIcon from '@mui/icons-material/ReceiptLong'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; +import ErrorOutlineIcon from '@mui/icons-material/WarningAmber'; import QrCode2Icon from '@mui/icons-material/QrCode2'; import PersonIcon from '@mui/icons-material/Person'; import CreditCardIcon from '@mui/icons-material/CreditCard'; import CurrencyRupeeIcon from '@mui/icons-material/CurrencyRupee'; -import { api } from '../../services/api'; +import { api, getFrontendMenuItem } from '../../services/api'; interface BillPaymentViewProps { onNavigate: (view: 'landing' | 'menu' | 'cart' | 'status' | 'bill' | 'voice') => void; } +type PayOutcome = 'idle' | 'success' | 'failure'; + +declare global { + interface Window { + Razorpay?: new (options: Record) => { open: () => void }; + } +} + +async function loadRazorpayScript(): Promise { + if (window.Razorpay) return; + await new Promise((resolve, reject) => { + const existing = document.querySelector('script[data-razorpay="1"]'); + if (existing) { + existing.addEventListener('load', () => resolve()); + existing.addEventListener('error', () => reject(new Error('Razorpay script failed'))); + return; + } + const script = document.createElement('script'); + script.src = 'https://checkout.razorpay.com/v1/checkout.js'; + script.async = true; + script.dataset.razorpay = '1'; + script.onload = () => resolve(); + script.onerror = () => reject(new Error('Failed to load Razorpay checkout')); + document.body.appendChild(script); + }); +} + export const BillPaymentView: React.FC = ({ onNavigate }) => { const [splitCount, setSplitCount] = useState(1); const [tipPercentage, setTipPercentage] = useState(10); - const [paymentDone, setPaymentDone] = useState(false); + const [outcome, setOutcome] = useState('idle'); const [paymentMethod, setPaymentMethod] = useState<'upi' | 'card' | 'cash' | null>(null); - const [billItems, setBillItems] = useState([]); + const [billItems, setBillItems] = useState< + { name: string; qty: number; price: number }[] + >([]); const [ticketNum, setTicketNum] = useState('---'); const [tableNum, setTableNum] = useState('12'); - const [billId, setBillId] = useState(''); + const [billId, setBillId] = useState(null); + const [billTotal, setBillTotal] = useState(0); + const [billSubtotal, setBillSubtotal] = useState(0); + const [cgstAmount, setCgstAmount] = useState(0); + const [sgstAmount, setSgstAmount] = useState(0); + const [loading, setLoading] = useState(true); + const [paying, setPaying] = useState(false); + const [error, setError] = useState(null); + const [cashHint, setCashHint] = useState(false); - const sessionId = sessionStorage.getItem('customer_session_id') || '112'; + const sessionId = sessionStorage.getItem('customer_session_id') || ''; - const loadBill = async () => { + const markSuccess = useCallback((method: 'upi' | 'card' | 'cash') => { + setPaymentMethod(method); + setOutcome('success'); + const url = new URL(window.location.href); + url.searchParams.set('pay', 'success'); + if (billId) url.searchParams.set('bill_id', String(billId)); + window.history.replaceState({}, '', url.toString()); + }, [billId]); + + const markFailure = useCallback((message?: string) => { + setOutcome('failure'); + if (message) setError(message); + const url = new URL(window.location.href); + url.searchParams.set('pay', 'failure'); + if (billId) url.searchParams.set('bill_id', String(billId)); + window.history.replaceState({}, '', url.toString()); + }, [billId]); + + const loadBill = useCallback(async () => { + setLoading(true); + setError(null); try { - const order = await api.getOrderForSession(sessionId); - if (order) { - setTicketNum(order.ticketNumber || '105'); - setTableNum(order.tableNumber || sessionStorage.getItem('customer_table_id') || '12'); - - setBillItems(order.items.map((it: any) => ({ - name: it.name, - qty: it.quantity, - price: it.price || 150 - }))); + const payParam = new URLSearchParams(window.location.search).get('pay'); + if (payParam === 'success') setOutcome('success'); + if (payParam === 'failure') setOutcome('failure'); - const bill = await api.generateBill(order.id); - if (bill) { - setBillId(bill.id); + if (!sessionId) { + setError('No active table session. Scan the QR code to start.'); + return; + } + const order = await api.getOrderForSession(sessionId); + if (!order) { + setError('No order found for this table yet.'); + return; + } + + setTicketNum(String(order.id ?? '---')); + setTableNum(sessionStorage.getItem('customer_table_id') || '12'); + + const items = (order.items || []).map((it: any) => { + const menu = getFrontendMenuItem?.(it.menu_item_id); + return { + name: it.name || menu?.name || `Dish ${it.menu_item_id}`, + qty: Number(it.quantity) || 1, + price: Number(it.unit_price ?? it.price ?? menu?.price ?? 0), + }; + }); + setBillItems(items); + + const bill = await api.sessionGenerateBill(order.id); + if (bill) { + setBillId(Number(bill.id)); + setBillSubtotal(Number(bill.subtotal)); + setCgstAmount(Number(bill.cgst_amount)); + setSgstAmount(Number(bill.sgst_amount)); + setBillTotal(Number(bill.total_amount)); + if (bill.status === 'paid') { + setOutcome('success'); + setPaymentMethod('upi'); } } - } catch (e) { + } catch (e: any) { console.error('Failed to load bill', e); + setError(e?.message || 'Failed to load bill'); + } finally { + setLoading(false); } - }; + }, [sessionId]); useEffect(() => { loadBill(); - }, []); + }, [loadBill]); - const subtotal = billItems.reduce((acc, i) => acc + i.price * i.qty, 0); - const cgst = subtotal * 0.025; - const sgst = subtotal * 0.025; - const serviceCharge = subtotal * 0.05; - const tip = subtotal * (tipPercentage / 100); - const totalBill = subtotal + cgst + sgst + serviceCharge + tip; - const perPersonPrice = totalBill / splitCount; + const tip = billSubtotal * (tipPercentage / 100); + const displayTotal = billTotal + tip; + const perPersonPrice = displayTotal / splitCount; - const handlePay = async (method: 'upi' | 'card' | 'cash') => { + const confirmMockCheckout = async ( + checkout: any, + method: 'upi' | 'card' + ) => { + await api.sessionConfirmRazorpay(checkout.bill_id, { + razorpay_order_id: checkout.razorpay_order_id, + razorpay_payment_id: `pay_mock_${Date.now()}`, + razorpay_signature: 'mock', + amount: Number(checkout.amount), + method, + }); + markSuccess(method); + }; + + const openLiveCheckout = async (checkout: any, method: 'upi' | 'card') => { + await loadRazorpayScript(); + if (!window.Razorpay) { + throw new Error('Razorpay checkout unavailable'); + } + const amountPaise = Math.round(Number(checkout.amount) * 100); + const rzp = new window.Razorpay({ + key: checkout.key_id, + amount: amountPaise, + currency: checkout.currency || 'INR', + name: 'RestroAI', + description: `Bill #${checkout.bill_id}`, + order_id: checkout.razorpay_order_id, + handler: async (response: { + razorpay_order_id: string; + razorpay_payment_id: string; + razorpay_signature: string; + }) => { + try { + await api.sessionConfirmRazorpay(checkout.bill_id, { + razorpay_order_id: response.razorpay_order_id, + razorpay_payment_id: response.razorpay_payment_id, + razorpay_signature: response.razorpay_signature, + amount: Number(checkout.amount), + method, + }); + markSuccess(method); + } catch (err: any) { + markFailure(err?.message || 'Payment confirmation failed'); + } + }, + modal: { + ondismiss: () => { + markFailure('Payment cancelled'); + }, + }, + theme: { color: '#ac2d00' }, + }); + rzp.open(); + }; + + const handleGatewayPay = async (method: 'upi' | 'card') => { + if (!billId) { + setError('Bill not ready yet'); + return; + } + setPaying(true); + setError(null); + setCashHint(false); try { - if (billId) { - await api.recordPayment(billId, method, totalBill); + const checkout = await api.sessionCheckoutRazorpay(billId); + if (checkout.mock) { + await confirmMockCheckout(checkout, method); + } else { + await openLiveCheckout(checkout, method); } - setPaymentMethod(method); - setPaymentDone(true); - } catch (e) { - alert('Failed to record payment: ' + e); + } catch (e: any) { + markFailure(e?.message || 'Checkout failed'); + } finally { + setPaying(false); } }; + const handleCash = () => { + setCashHint(true); + setPaymentMethod('cash'); + }; + + if (loading) { + return ( + + + + ); + } + return ( - {/* Header */} = ({ onNavigate }) Bill & Payment - 🪑 Table {tableNum} · Ticket #{ticketNum} + Table {tableNum} · Order #{ticketNum} + {billId ? ` · Bill #${billId}` : ''} } - label={paymentDone ? 'PAID ✓' : 'UNPAID'} - color={paymentDone ? 'success' : 'error'} + label={outcome === 'success' ? 'PAID ✓' : outcome === 'failure' ? 'FAILED' : 'UNPAID'} + color={outcome === 'success' ? 'success' : outcome === 'failure' ? 'warning' : 'error'} sx={{ fontWeight: 800 }} /> - {paymentDone ? ( + {error && ( + + {error} + + )} + + {outcome === 'success' ? ( = ({ onNavigate }) Payment Successful! - ₹{totalBill.toFixed(0)} paid via {paymentMethod === 'upi' ? 'UPI' : paymentMethod === 'card' ? 'Card' : 'Cash'} + ₹{billTotal.toFixed(0)} paid + {paymentMethod + ? ` via ${paymentMethod === 'upi' ? 'UPI' : paymentMethod === 'card' ? 'Card' : 'Cash'}` + : ''} - Thank you for dining with RestroAI! Your receipt has been sent. + Thank you for dining with RestroAI! + ) : outcome === 'failure' ? ( + + + + Payment not completed + + + You can try again or ask staff for help. + + + ) : ( <> - {/* Itemized Bill */} - - + + - 🧾 Bill Details - - - GSTIN: 29XXXXXX0001Z5 + Bill Details @@ -174,32 +387,48 @@ export const BillPaymentView: React.FC = ({ onNavigate }) {[ - { label: 'Subtotal', value: `₹${subtotal.toFixed(0)}` }, - { label: 'CGST (2.5%)', value: `₹${cgst.toFixed(0)}` }, - { label: 'SGST (2.5%)', value: `₹${sgst.toFixed(0)}` }, - { label: 'Service Charge (5%)', value: `₹${serviceCharge.toFixed(0)}` }, - { label: `Staff Tip (${tipPercentage}%)`, value: `₹${tip.toFixed(0)}` }, + { label: 'Subtotal', value: `₹${billSubtotal.toFixed(0)}` }, + { label: 'CGST', value: `₹${cgstAmount.toFixed(0)}` }, + { label: 'SGST', value: `₹${sgstAmount.toFixed(0)}` }, + { label: `Suggested tip (${tipPercentage}%)`, value: `₹${tip.toFixed(0)}` }, ].map((row) => ( - {row.label} - {row.value} + + {row.label} + + + {row.value} + ))} - Total Amount + + Pay now + - ₹{totalBill.toFixed(0)} + ₹{billTotal.toFixed(0)} + + Gateway charges bill total (tip is optional / cash). + - {/* Split Bill */} - + - 👥 Split the Bill + Split the Bill {[1, 2, 3, 4].map((count) => ( @@ -217,15 +446,23 @@ export const BillPaymentView: React.FC = ({ onNavigate }) {splitCount > 1 && ( - Each person pays: ₹{perPersonPrice.toFixed(0)} + Each person ≈ ₹{perPersonPrice.toFixed(0)} (incl. suggested tip) )} - {/* Tip Selector */} - + - ❤️ Add Tip for Staff + Suggested tip {[0, 10, 15, 20].map((pct) => ( @@ -242,19 +479,33 @@ export const BillPaymentView: React.FC = ({ onNavigate }) - {/* Payment Options */} - + {cashHint && ( + + Please pay cash to your waiter. They will mark the bill paid at the counter. + + )} + + Choose Payment Method - {/* UPI Button */} @@ -273,9 +524,16 @@ export const BillPaymentView: React.FC = ({ onNavigate }) fullWidth variant="outlined" size="large" - onClick={() => handlePay('card')} + disabled={paying || !billId} + onClick={() => handleGatewayPay('card')} startIcon={} - sx={{ py: 1.5, borderRadius: '12px', fontWeight: 800, color: '#ac2d00', borderColor: '#ac2d00' }} + sx={{ + py: 1.5, + borderRadius: '12px', + fontWeight: 800, + color: '#ac2d00', + borderColor: '#ac2d00', + }} > Debit / Credit Card @@ -285,9 +543,16 @@ export const BillPaymentView: React.FC = ({ onNavigate }) fullWidth variant="outlined" size="large" - onClick={() => handlePay('cash')} + disabled={paying} + onClick={handleCash} startIcon={} - sx={{ py: 1.5, borderRadius: '12px', fontWeight: 800, color: '#2e7d32', borderColor: '#2e7d32' }} + sx={{ + py: 1.5, + borderRadius: '12px', + fontWeight: 800, + color: '#2e7d32', + borderColor: '#2e7d32', + }} > Pay by Cash diff --git a/src/components/customer/CartPeekDrawer.tsx b/src/components/customer/CartPeekDrawer.tsx new file mode 100644 index 0000000..80c017e --- /dev/null +++ b/src/components/customer/CartPeekDrawer.tsx @@ -0,0 +1,234 @@ +import React from 'react'; +import { + Box, + Typography, + Drawer, + Button, + IconButton, + Divider, + CardMedia, +} from '@mui/material'; +import AddIcon from '@mui/icons-material/Add'; +import RemoveIcon from '@mui/icons-material/Remove'; +import DeleteIcon from '@mui/icons-material/Delete'; +import ShoppingCartCheckoutIcon from '@mui/icons-material/ShoppingCartCheckout'; +import CloseIcon from '@mui/icons-material/Close'; +import type { MenuItem } from '../../data/menuData'; + +export type CartLine = { + item: MenuItem; + qty: number; + notes?: string; +}; + +interface CartPeekDrawerProps { + open: boolean; + lines: CartLine[]; + subtotal: number; + onClose: () => void; + onUpdateCart: (itemId: string, quantity: number) => void; + onCheckout: () => void; + onContinueShopping: () => void; +} + +export const CartPeekDrawer: React.FC = ({ + open, + lines, + subtotal, + onClose, + onUpdateCart, + onCheckout, + onContinueShopping, +}) => { + const totalUnits = lines.reduce((sum, l) => sum + l.qty, 0); + + return ( + + + + + + + + Your cart + + + {totalUnits === 0 + ? 'No items yet' + : `${totalUnits} item${totalUnits === 1 ? '' : 's'} · ₹${subtotal.toFixed(0)}`} + + + + + + + + {lines.length === 0 ? ( + + Cart is empty + + Add dishes from the menu, then check them here anytime. + + + + ) : ( + <> + + {lines.map(({ item, qty, notes }) => ( + + + + + {item.name} + + + ₹{item.price} · ₹{(item.price * qty).toFixed(0)} + + {notes ? ( + + {notes} + + ) : null} + + + + onUpdateCart(String(item.id), qty - 1)} + sx={{ color: '#fff', p: 0.5 }} + aria-label={`Decrease ${item.name}`} + > + + + + {qty} + + onUpdateCart(String(item.id), qty + 1)} + sx={{ color: '#fff', p: 0.5 }} + aria-label={`Increase ${item.name}`} + > + + + + onUpdateCart(String(item.id), 0)} + aria-label={`Remove ${item.name}`} + > + + + + + ))} + + + + + + Subtotal + + ₹{subtotal.toFixed(0)} + + + + + + + + + )} + + + ); +}; diff --git a/src/components/customer/CartReviewView.tsx b/src/components/customer/CartReviewView.tsx index ce0feee..b38988b 100644 --- a/src/components/customer/CartReviewView.tsx +++ b/src/components/customer/CartReviewView.tsx @@ -11,6 +11,9 @@ import { Chip, Card, CardMedia, + BottomNavigation, + BottomNavigationAction, + Badge, } from '@mui/material'; import ArrowBackIcon from '@mui/icons-material/ArrowBack'; import AddIcon from '@mui/icons-material/Add'; @@ -18,41 +21,65 @@ import RemoveIcon from '@mui/icons-material/Remove'; import DeleteIcon from '@mui/icons-material/Delete'; import SendIcon from '@mui/icons-material/Send'; import ReceiptLongIcon from '@mui/icons-material/ReceiptLong'; +import HomeIcon from '@mui/icons-material/Home'; +import MenuBookIcon from '@mui/icons-material/MenuBook'; +import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; +import TrackChangesIcon from '@mui/icons-material/TrackChanges'; import { MENU_ITEMS } from '../../data/menuData'; import type { MenuItem } from '../../data/menuData'; import { StatusBadge } from '../common/StatusBadge'; interface CartReviewViewProps { cart: { [itemId: string]: number }; + cartNotes?: { [itemId: string]: string }; + menuItems?: MenuItem[]; onUpdateCart: (itemId: string, quantity: number) => void; onPlaceOrder: (orderNotes: string) => void; onNavigate: (view: 'landing' | 'menu' | 'cart' | 'status' | 'bill' | 'voice') => void; } +const tableLabel = () => + sessionStorage.getItem('customer_table_number') || + sessionStorage.getItem('customer_table_id') || + '—'; + export const CartReviewView: React.FC = ({ cart, + cartNotes = {}, + menuItems = MENU_ITEMS, onUpdateCart, onPlaceOrder, onNavigate, }) => { const [orderNotes, setOrderNotes] = useState(''); + const [placing, setPlacing] = useState(false); const cartEntries = Object.entries(cart) .map(([id, qty]) => { - const item = MENU_ITEMS.find((m) => m.id === id); - return item ? { item, qty } : null; + const item = menuItems.find((m) => String(m.id) === id); + return item ? { item, qty, notes: cartNotes[id] } : null; }) - .filter(Boolean) as { item: MenuItem; qty: number }[]; + .filter(Boolean) as { item: MenuItem; qty: number; notes?: string }[]; + const totalUnits = cartEntries.reduce((sum, e) => sum + e.qty, 0); const subtotal = cartEntries.reduce((sum, entry) => sum + entry.item.price * entry.qty, 0); - const cgst = subtotal * 0.025; // CGST 2.5% - const sgst = subtotal * 0.025; // SGST 2.5% - const serviceCharge = subtotal * 0.05; // 5% service charge + const cgst = subtotal * 0.025; + const sgst = subtotal * 0.025; + const serviceCharge = subtotal * 0.05; const grandTotal = subtotal + cgst + sgst + serviceCharge; + const handlePlace = async () => { + if (placing || cartEntries.length === 0) return; + setPlacing(true); + try { + await Promise.resolve(onPlaceOrder(orderNotes)); + } finally { + setPlacing(false); + } + }; + return ( - - {/* Header */} + = ({ boxShadow: '0 2px 8px rgba(0,0,0,0.05)', }} > - - onNavigate('menu')} size="small"> + + onNavigate('menu')} size="small" aria-label="Back to menu"> - - - Your Cart + + + Review order - 🪑 Table {sessionStorage.getItem('customer_table_id') || '12'} + Table {tableLabel()} · Check items before placing @@ -93,22 +120,29 @@ export const CartReviewView: React.FC = ({ elevation={0} sx={{ p: 5, textAlign: 'center', borderRadius: '20px', border: '2px dashed #e4ddd8', mt: 4 }} > + 🛒 Your cart is empty - Go back and add some delicious dishes! + Browse the menu and add dishes. You can check your cart anytime while ordering. - ) : ( <> - {/* Cart Items */} + + Items in your cart + - {cartEntries.map(({ item, qty }) => ( + {cartEntries.map(({ item, qty, notes }) => ( = ({ alt={item.name} /> - + {item.name} - - ₹{(item.price * qty).toFixed(0)} + + ₹{item.price} each - + {notes ? ( + + {notes} + + ) : null} + - onUpdateCart(item.id, qty - 1)}> + onUpdateCart(String(item.id), qty - 1)}> - + {qty} - onUpdateCart(item.id, qty + 1)}> + onUpdateCart(String(item.id), qty + 1)}> - onUpdateCart(item.id, 0)}> - - + + ₹{(item.price * qty).toFixed(0)} + onUpdateCart(String(item.id), 0)} aria-label="Remove"> + + + ))} - {/* Instructions */} + + - 📝 Special Instructions + Special instructions (optional) = ({ /> - {/* Bill Summary */} - + - Bill Summary + Bill summary {[ - { label: 'Items Total', value: `₹${subtotal.toFixed(0)}` }, + { label: 'Items total', value: `₹${subtotal.toFixed(0)}` }, { label: 'CGST (2.5%)', value: `₹${cgst.toFixed(0)}` }, { label: 'SGST (2.5%)', value: `₹${sgst.toFixed(0)}` }, - { label: 'Service Charge (5%)', value: `₹${serviceCharge.toFixed(0)}` }, + { label: 'Service charge (5%)', value: `₹${serviceCharge.toFixed(0)}` }, ].map((row) => ( - - {row.label} - {row.value} + + + {row.label} + + + {row.value} + ))} - Grand Total - ₹{grandTotal.toFixed(0)} + + To pay + + + ₹{grandTotal.toFixed(0)} + - *Inclusive of all taxes as per GST regulations + Inclusive of taxes · Review carefully before placing + + )} + - {/* Place Order CTA */} + {cartEntries.length > 0 && ( + + - - )} - + + + )} + + + { + if (v === 0) onNavigate('landing'); + if (v === 1) onNavigate('menu'); + if (v === 3) onNavigate('status'); + }} + sx={{ bgcolor: '#ffffff', height: 64 }} + > + } sx={{ '&.Mui-selected': { color: '#ac2d00' } }} /> + } sx={{ '&.Mui-selected': { color: '#ac2d00' } }} /> + + + + } + sx={{ '&.Mui-selected': { color: '#ac2d00' } }} + /> + } + sx={{ '&.Mui-selected': { color: '#ac2d00' } }} + /> + + ); }; diff --git a/src/components/customer/CustomizationModal.tsx b/src/components/customer/CustomizationModal.tsx index 012e58d..23fd816 100644 --- a/src/components/customer/CustomizationModal.tsx +++ b/src/components/customer/CustomizationModal.tsx @@ -44,8 +44,8 @@ export const CustomizationModal: React.FC = ({ const handleAdd = () => { const formattedNotes = [ `Spice: ${spiceLevel}`, - extraCheese ? 'Extra Cheese (+ $2.50)' : '', - extraTruffle ? 'Extra Truffle Oil (+ $3.00)' : '', + extraCheese ? 'Extra Cheese (+₹40)' : '', + extraTruffle ? 'Extra Truffle Oil (+₹60)' : '', specialNotes ? `Note: ${specialNotes}` : '', ] .filter(Boolean) diff --git a/src/components/customer/MenuBrowseView.tsx b/src/components/customer/MenuBrowseView.tsx index ccc6845..7fb8cf0 100644 --- a/src/components/customer/MenuBrowseView.tsx +++ b/src/components/customer/MenuBrowseView.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo } from 'react'; +import React, { useMemo, useState } from 'react'; import { Box, Typography, @@ -13,6 +13,8 @@ import { BottomNavigation, BottomNavigationAction, Button, + Snackbar, + Alert, } from '@mui/material'; import AddIcon from '@mui/icons-material/Add'; import RemoveIcon from '@mui/icons-material/Remove'; @@ -27,9 +29,13 @@ import StarIcon from '@mui/icons-material/Star'; import { MENU_ITEMS, CATEGORIES } from '../../data/menuData'; import type { MenuItem, CategoryType } from '../../data/menuData'; import { StatusBadge } from '../common/StatusBadge'; +import { useLocale } from '../../i18n/LocaleContext'; +import { CartPeekDrawer } from './CartPeekDrawer'; interface MenuBrowseViewProps { cart: { [itemId: string]: number }; + cartNotes?: { [itemId: string]: string }; + menuItems?: MenuItem[]; onUpdateCart: (itemId: string, quantity: number) => void; onOpenCustomization: (item: MenuItem) => void; onOpenVoice: () => void; @@ -50,26 +56,49 @@ const SPICE_ICONS: Record = { 'extra-hot': '🔥', }; +const tableLabel = () => + sessionStorage.getItem('customer_table_number') || + sessionStorage.getItem('customer_table_id') || + '—'; + export const MenuBrowseView: React.FC = ({ cart, + cartNotes = {}, + menuItems = MENU_ITEMS, onUpdateCart, onOpenCustomization, onOpenVoice, onNavigate, }) => { + const { t } = useLocale(); const [activeCategory, setActiveCategory] = useState('All'); const [searchQuery, setSearchQuery] = useState(''); const [dietaryFilter, setDietaryFilter] = useState(null); - const [bottomNav, setBottomNav] = useState(1); // 1 = Menu + const [bottomNav, setBottomNav] = useState(1); + const [cartPeekOpen, setCartPeekOpen] = useState(false); + const [emptyHint, setEmptyHint] = useState(false); const totalCartItems = Object.values(cart).reduce((a, b) => a + b, 0); const totalCartValue = Object.entries(cart).reduce((sum, [id, qty]) => { - const item = MENU_ITEMS.find((m) => m.id === id); + const item = menuItems.find((m) => String(m.id) === id); return sum + (item ? item.price * qty : 0); }, 0); + const cartLines = useMemo( + () => + Object.entries(cart) + .map(([id, qty]) => { + const item = menuItems.find((m) => String(m.id) === id); + if (!item) return null; + return { item, qty, notes: cartNotes[id] }; + }) + .filter(Boolean) as { item: MenuItem; qty: number; notes?: string }[], + [cart, cartNotes, menuItems] + ); + const filteredItems = useMemo(() => { - return MENU_ITEMS.filter((item) => { + return menuItems.filter((item) => { + if (item.isAvailable === false) return false; const matchCategory = activeCategory === 'All' || item.category === activeCategory; const matchSearch = !searchQuery || @@ -79,20 +108,31 @@ export const MenuBrowseView: React.FC = ({ const matchDietary = !dietaryFilter || item.dietary === dietaryFilter; return matchCategory && matchSearch && matchDietary; }); - }, [activeCategory, searchQuery, dietaryFilter]); + }, [menuItems, activeCategory, searchQuery, dietaryFilter]); + + const openCartCheck = () => { + setCartPeekOpen(true); + }; + + const handleCartTap = () => { + openCartCheck(); + if (totalCartItems === 0) setEmptyHint(true); + }; const handleBottomNav = (_: React.SyntheticEvent, newValue: number) => { setBottomNav(newValue); if (newValue === 0) onNavigate('landing'); + if (newValue === 1) setBottomNav(1); if (newValue === 2) { - if (totalCartItems > 0) onNavigate('cart'); + openCartCheck(); + if (totalCartItems === 0) setEmptyHint(true); + setBottomNav(1); } if (newValue === 3) onNavigate('status'); }; return ( - {/* Sticky Header */} = ({ boxShadow: '0 2px 12px rgba(0,0,0,0.05)', }} > - {/* Top bar */} - + - RestroAI Menu + Menu - 🪑 Table {sessionStorage.getItem('customer_table_id') || '12'} · Dine-In + Table {tableLabel()} · Tap dishes to add · Check cart anytime - + totalCartItems > 0 && onNavigate('cart')} + onClick={handleCartTap} + aria-label="View cart" sx={{ bgcolor: totalCartItems > 0 ? '#ac2d00' : '#f2ede9', color: totalCartItems > 0 ? '#fff' : '#8f7068', @@ -139,12 +176,11 @@ export const MenuBrowseView: React.FC = ({ - {/* Search Bar */} setSearchQuery(e.target.value)} slotProps={{ @@ -166,21 +202,26 @@ export const MenuBrowseView: React.FC = ({ /> - {/* Category Tabs - Horizontal Scroll */} {CATEGORIES.map((cat) => { const catEmoji: Record = { - All: '🍽️', Starters: '🥗', Biryani: '🍚', Mains: '🍛', - Breads: '🫓', Drinks: '🥤', Desserts: '🍮', Specials: '⭐', + All: '🍽️', + Starters: '🥗', + Biryani: '🍚', + Mains: '🍛', + Breads: '🫓', + Drinks: '🥤', + Desserts: '🍮', + Specials: '⭐', }; const isActive = activeCategory === cat; return ( @@ -195,15 +236,13 @@ export const MenuBrowseView: React.FC = ({ color: isActive ? '#fff' : '#5b4139', border: isActive ? '2px solid #ac2d00' : '1.5px solid #e4ddd8', '&:hover': { bgcolor: isActive ? '#872100' : '#f5ede9' }, - transition: 'all 0.15s ease', }} /> ); })} - {/* Dietary Filters */} - + {DIETARY_FILTERS.map((f) => ( = ({ - {/* AI Banner */} - + = ({ > - + - 🍛 Today's AI Pick: Hyderabadi Chicken Biryani + Order with AI voice - - Tap to voice order or browse below · ₹380 + + Say what you want — we add it to your cart - {/* Menu Cards */} - + 0 ? 22 : 14 }}> {activeCategory !== 'All' && ( - - {activeCategory} ({filteredItems.length} items) + + {activeCategory} ({filteredItems.length}) )} {filteredItems.length === 0 ? ( - No dishes found - Try a different search or filter + + No dishes found + + + Try a different search or filter + + ) : ( - + {filteredItems.map((item) => { - const qty = cart[item.id] || 0; + const qty = cart[String(item.id)] || 0; return ( 0 ? '1.5px solid #ac2d00' : '1px solid #f0ebe7', overflow: 'hidden', bgcolor: '#ffffff', - boxShadow: '0 2px 12px rgba(0,0,0,0.04)', - transition: 'box-shadow 0.2s ease', - '&:hover': { boxShadow: '0 6px 20px rgba(0,0,0,0.08)' }, + display: 'flex', + boxShadow: '0 2px 10px rgba(0,0,0,0.04)', }} > - {/* Food Image */} - + - {/* Badges on image */} - - {item.isChefSpecial && ( - } - label="Chef's Special" - size="small" - sx={{ - bgcolor: 'rgba(172,45,0,0.9)', - color: '#fff', - fontWeight: 800, - fontSize: '0.65rem', - backdropFilter: 'blur(4px)', - }} - /> - )} - {item.isBestseller && ( - } - label="Bestseller" - size="small" - sx={{ - bgcolor: 'rgba(0,0,0,0.7)', - color: '#FFD700', - fontWeight: 800, - fontSize: '0.65rem', - backdropFilter: 'blur(4px)', - }} - /> - )} - - {item.spiceLevel && ( + {(item.isChefSpecial || item.isBestseller) && ( + ) : ( + + ) + } + label={item.isChefSpecial ? 'Special' : 'Hit'} size="small" sx={{ position: 'absolute', - top: 10, - right: 10, - bgcolor: 'rgba(255,255,255,0.9)', - fontWeight: 700, - fontSize: '0.65rem', - textTransform: 'capitalize', + top: 6, + left: 6, + height: 22, + fontSize: '0.62rem', + fontWeight: 800, + bgcolor: 'rgba(0,0,0,0.7)', + color: '#fff', }} /> )} - {/* Card Content */} - - - - - {item.name} - - {item.nameHindi && ( - - {item.nameHindi} - - )} - + + + + {item.name} + - - + {item.description} - - {/* Tags */} - {item.tags && item.tags.length > 0 && ( - - {item.tags.slice(0, 3).map((tag) => ( - - ))} - - )} - - {/* Price + Action Row */} - + - - ₹{item.price} - + ₹{item.price} - ⏱ {item.prepTimeMinutes} min + {item.spiceLevel ? `${SPICE_ICONS[item.spiceLevel] || ''} ` : ''} + {item.prepTimeMinutes} min {qty === 0 ? ( )} - {/* Bottom Navigation */} = ({ borderTop: '1px solid #f0ebe7', }} > - + + } sx={{ '&.Mui-selected': { color: '#ac2d00' } }} /> + } sx={{ '&.Mui-selected': { color: '#ac2d00' } }} /> } - sx={{ '&.Mui-selected': { color: '#ac2d00' } }} - /> - } - sx={{ '&.Mui-selected': { color: '#ac2d00' } }} - /> - @@ -531,12 +547,37 @@ export const MenuBrowseView: React.FC = ({ sx={{ '&.Mui-selected': { color: '#ac2d00' } }} /> } sx={{ '&.Mui-selected': { color: '#ac2d00' } }} /> + + setCartPeekOpen(false)} + onUpdateCart={onUpdateCart} + onContinueShopping={() => setCartPeekOpen(false)} + onCheckout={() => { + setCartPeekOpen(false); + onNavigate('cart'); + }} + /> + + setEmptyHint(false)} + anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} + sx={{ bottom: { xs: 140, sm: 140 } }} + > + setEmptyHint(false)} sx={{ fontWeight: 700, borderRadius: '12px' }}> + Cart is empty — add a dish, then check it here anytime. + + ); }; diff --git a/src/components/customer/OrderStatusView.tsx b/src/components/customer/OrderStatusView.tsx index 2026c1c..ca95928 100644 --- a/src/components/customer/OrderStatusView.tsx +++ b/src/components/customer/OrderStatusView.tsx @@ -22,7 +22,15 @@ import DeliveryDiningIcon from '@mui/icons-material/DeliveryDining'; import SentimentSatisfiedAltIcon from '@mui/icons-material/SentimentSatisfiedAlt'; import { api, RestroWebSocket, subscribeToWsEvents } from '../../services/api'; +interface PlacedItem { + name: string; + qty: number; + price: number; + notes?: string; +} + interface OrderStatusViewProps { + placedItems?: PlacedItem[]; onNavigate: (view: 'landing' | 'menu' | 'cart' | 'status' | 'bill' | 'voice') => void; } @@ -53,14 +61,34 @@ const ORDER_STEPS = [ }, ]; -export const OrderStatusView: React.FC = ({ onNavigate }) => { +export const OrderStatusView: React.FC = ({ + placedItems = [], + onNavigate, +}) => { const [activeStep, setActiveStep] = useState(0); const [ticketNum, setTicketNum] = useState('---'); - const [tableNum, setTableNum] = useState('12'); + const [tableNum, setTableNum] = useState( + () => + sessionStorage.getItem('customer_table_number') || + sessionStorage.getItem('customer_table_id') || + '—' + ); const [waiterNotified, setWaiterNotified] = useState(false); const [eta, setEta] = useState(15); const sessionId = sessionStorage.getItem('customer_session_id') || '112'; + const summaryItems = + placedItems.length > 0 + ? placedItems + : (() => { + try { + const raw = sessionStorage.getItem('customer_last_order_items'); + return raw ? (JSON.parse(raw) as PlacedItem[]) : []; + } catch { + return [] as PlacedItem[]; + } + })(); + const summaryTotal = summaryItems.reduce((s, i) => s + i.qty * i.price, 0); const loadOrder = async () => { try { @@ -211,33 +239,54 @@ export const OrderStatusView: React.FC = ({ onNavigate }) - {/* Ordered Items Summary */} - 🧾 Your Order Summary + Your order summary - {[ - { name: 'Hyderabadi Chicken Biryani', qty: 1, price: 380 }, - { name: 'Paneer Tikka', qty: 1, price: 280 }, - { name: 'Garlic Naan', qty: 2, price: 80 }, - { name: 'Mango Lassi', qty: 2, price: 120 }, - ].map((item, idx) => ( - - - {item.qty}× {item.name} - - - ₹{item.qty * item.price} - - - ))} - - Estimated Total - ₹960 - + {summaryItems.length === 0 ? ( + + Order details will appear here after you place an order from the cart. + + ) : ( + <> + {summaryItems.map((item, idx) => ( + + + + {item.qty}× {item.name} + + {item.notes ? ( + + {item.notes} + + ) : null} + + + ₹{item.qty * item.price} + + + ))} + + + Items total + + + ₹{summaryTotal.toFixed(0)} + + + + )} - {/* Action Buttons */} diff --git a/src/components/customer/TableLandingView.tsx b/src/components/customer/TableLandingView.tsx index eccb312..5315aef 100644 --- a/src/components/customer/TableLandingView.tsx +++ b/src/components/customer/TableLandingView.tsx @@ -8,6 +8,14 @@ import { Chip, Grid, IconButton, + Dialog, + DialogTitle, + DialogContent, + List, + ListItemButton, + ListItemText, + CircularProgress, + Alert, } from '@mui/material'; import RestaurantMenuIcon from '@mui/icons-material/RestaurantMenu'; import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'; @@ -17,44 +25,83 @@ import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; import WifiIcon from '@mui/icons-material/Wifi'; import MicIcon from '@mui/icons-material/Mic'; import LockIcon from '@mui/icons-material/Lock'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import BadgeIcon from '@mui/icons-material/Badge'; +import { useLocale } from '../../i18n/LocaleContext'; +import { uiLangToLocale } from '../../i18n/messages'; + +export type GuestTableOption = { + id: number; + table_number: string; + capacity: number; + status: string; +}; interface TableLandingViewProps { - onStartOrdering: (language: string) => void; + onStartOrdering: (language: 'english' | 'hindi' | 'hinglish') => void; onOpenStaffLogin?: () => void; onOpenVoice?: () => void; + selectedTable: GuestTableOption | null; + tableLocked: boolean; + availableTables: GuestTableOption[]; + tablesLoading?: boolean; + tablesError?: string | null; + onSelectTable: (table: GuestTableOption) => void; } export const TableLandingView: React.FC = ({ onStartOrdering, onOpenStaffLogin, onOpenVoice, + selectedTable, + tableLocked, + availableTables, + tablesLoading = false, + tablesError = null, + onSelectTable, }) => { + const { t, setLocale } = useLocale(); const [selectedLang, setSelectedLang] = useState<'english' | 'hindi' | 'hinglish'>('english'); + const [pickerOpen, setPickerOpen] = useState(false); const languages = [ { id: 'english', emoji: '🇬🇧', name: 'English', - subText: 'English', icon: , }, { id: 'hindi', emoji: '🇮🇳', name: 'हिन्दी', - subText: 'Hindi', icon: नमस्ते, }, { id: 'hinglish', emoji: '🤝', name: 'Hinglish', - subText: 'Mix', icon: , }, ]; + const tableLabel = selectedTable + ? `${t('tableLabel')} ${selectedTable.table_number}` + : t('selectTable'); + + const openPicker = () => { + if (tableLocked) return; + setPickerOpen(true); + }; + + const handleStart = () => { + if (!selectedTable && !tableLocked) { + setPickerOpen(true); + return; + } + onStartOrdering(selectedLang); + }; + return ( = ({ flexDirection: 'column', }} > - {/* Hero Image Banner */} = ({ flexShrink: 0, }} > - {/* Dark overlay gradient */} = ({ }} /> - {/* Top bar inside hero */} = ({ - RestroAI + {t('brand')} { - const params = new URLSearchParams(window.location.search); - const tableParam = params.get('table'); - if (tableParam) { - const parsed = parseInt(tableParam); - if (!isNaN(parsed)) return parsed; - } - const path = window.location.pathname; - const match = path.match(/\/scan\/(\d+)/); - if (match) { - return parseInt(match[1]); - } - return 12; - })()}`} + clickable={!tableLocked} + onClick={openPicker} + icon={ + tableLocked ? ( + + ) : ( + + ) + } + label={tablesLoading ? '…' : `🪑 ${tableLabel}`} + title={tableLocked ? t('tableLockedHint') : t('tablePickHint')} sx={{ fontWeight: 800, fontFamily: '"JetBrains Mono", monospace', @@ -145,15 +186,21 @@ export const TableLandingView: React.FC = ({ color: '#fff', backdropFilter: 'blur(8px)', border: '1px solid rgba(255,255,255,0.35)', + cursor: tableLocked ? 'default' : 'pointer', + '& .MuiChip-icon': { ml: 0.5 }, }} /> - - + (onOpenStaffLogin ? onOpenStaffLogin() : (window.location.href = '/staff.html'))} + sx={{ color: 'rgba(255,255,255,0.7)' }} + aria-label={t('staffLogin')} + > + - {/* Hero text at bottom of image */} @@ -170,10 +217,48 @@ export const TableLandingView: React.FC = ({ - {/* Content Area */} - {/* AI Voice Section */} + {tablesError && ( + + {tablesError} + + )} + + {!tableLocked && ( + + + + {t('selectTable')} + + + {selectedTable ? `Table ${selectedTable.table_number}` : t('tablePickHint')} + + + + + )} + + {tableLocked && selectedTable && ( + } sx={{ mb: 2, fontWeight: 600 }}> + {t('tableLockedHint')} — Table {selectedTable.table_number} + + )} + = ({ color: '#fff', flexShrink: 0, boxShadow: '0 4px 12px rgba(172,45,0,0.4)', - animation: 'pulse 2.5s ease-in-out infinite', }} > - Order with Voice 🎙️ + {t('voiceOrder')} Tap & say your order in English or Hindi @@ -218,7 +302,6 @@ export const TableLandingView: React.FC = ({ - {/* Language Selection */} = ({ textTransform: 'uppercase', }} > - Choose Your Language + {t('chooseLanguage')} @@ -241,7 +324,11 @@ export const TableLandingView: React.FC = ({ setSelectedLang(lang.id as 'english' | 'hindi' | 'hinglish')} + onClick={() => { + const next = lang.id as 'english' | 'hindi' | 'hinglish'; + setSelectedLang(next); + setLocale(uiLangToLocale(next)); + }} sx={{ p: 2, borderRadius: '16px', @@ -271,7 +358,6 @@ export const TableLandingView: React.FC = ({ - {/* Today's Highlights */} = ({ - {/* Main CTA Button */} - {/* Footer */} @@ -333,6 +417,39 @@ export const TableLandingView: React.FC = ({ + + setPickerOpen(false)} fullWidth maxWidth="xs"> + {t('selectTable')} + + {tablesLoading && ( + + + + )} + {!tablesLoading && availableTables.length === 0 && ( + {t('noTablesAvailable')} + )} + + {availableTables.map((table) => ( + { + onSelectTable(table); + setPickerOpen(false); + }} + sx={{ borderRadius: 2, mb: 0.5 }} + > + + + ))} + + + ); }; diff --git a/src/components/customer/VoiceAssistantModal.tsx b/src/components/customer/VoiceAssistantModal.tsx index ce89777..4f791f0 100644 --- a/src/components/customer/VoiceAssistantModal.tsx +++ b/src/components/customer/VoiceAssistantModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from 'react'; +import React, { useState, useCallback, useRef, useEffect } from 'react'; import { Box, Drawer, @@ -10,6 +10,8 @@ import { Button, Divider, Fade, + CircularProgress, + TextField, } from '@mui/material'; import CloseIcon from '@mui/icons-material/Close'; import MicIcon from '@mui/icons-material/Mic'; @@ -18,58 +20,32 @@ import GraphicEqIcon from '@mui/icons-material/GraphicEq'; import SmartToyIcon from '@mui/icons-material/SmartToy'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import AddShoppingCartIcon from '@mui/icons-material/AddShoppingCart'; -import VolumeUpIcon from '@mui/icons-material/VolumeUp'; -import { MENU_ITEMS } from '../../data/menuData'; -import type { MenuItem } from '../../data/menuData'; +import SendIcon from '@mui/icons-material/Send'; import { useSpeechRecognition } from '../../hooks/useSpeechRecognition'; -import { useSpeechSynthesis } from '../../hooks/useSpeechSynthesis'; import type { VoiceState } from '../../hooks/useSpeechRecognition'; +import { api } from '../../services/api'; +import { debugLog, maskToken } from '../../utils/debugLog'; -interface DetectedItem { - menuItem: MenuItem; - qty: number; -} +export type AiCartLine = { + menu_item_id: number; + name: string; + quantity: number; + unit_price: number | string; + notes: string[]; + is_available: boolean; +}; interface VoiceAssistantModalProps { open: boolean; onClose: () => void; onAddToCart: (itemId: string, quantity: number) => void; -} - -// Detect menu items from voice transcript -function parseTranscriptToItems(text: string): DetectedItem[] { - const lower = text.toLowerCase(); - const detectedItems: DetectedItem[] = []; - - for (const item of MENU_ITEMS) { - const itemNameLower = item.name.toLowerCase(); - // Check for item name in transcript - if (lower.includes(itemNameLower) || (item.nameHindi && lower.includes(item.nameHindi.toLowerCase()))) { - // Try to detect quantity (look for digits near the item name) - let qty = 1; - const qtxMatch = lower.match(/(\d+)\s*(?:plate|order|piece|nos|number)?(?:of\s+)?(?:\w+\s+)*/); - if (qtxMatch) { - const num = parseInt(qtxMatch[1], 10); - if (num >= 1 && num <= 10) qty = num; - } - if (/\btwo\b|\bdo\b/i.test(lower)) qty = 2; - if (/\bthree\b|\bteen\b/i.test(lower)) qty = 3; - if (/\bfour\b|\bchar\b/i.test(lower)) qty = 4; - - // avoid duplicates - if (!detectedItems.find((d) => d.menuItem.id === item.id)) { - detectedItems.push({ menuItem: item, qty }); - } - } - } - - return detectedItems; + onSyncCart?: (cart: AiCartLine[]) => void; } const QUICK_COMMANDS = [ - { label: '2 Butter Chicken aur Naan', text: '2 butter chicken and 2 garlic naan please' }, - { label: 'Hyderabadi Chicken Biryani', text: 'one hyderabadi chicken biryani for table 12' }, - { label: 'Veg Thali with Lassi', text: "chef's thali and one mango lassi" }, + { label: '2 Butter Chicken aur Naan', text: '2 butter chicken kam spicy and 2 garlic naan please' }, + { label: 'Hyderabadi Chicken Biryani', text: 'one hyderabadi chicken biryani please' }, + { label: 'Veg Thali with Lassi', text: "chef's thali and one mango lassi please" }, { label: 'Paneer Tikka Starter', text: 'paneer tikka and masala chai' }, { label: 'Seekh Kebab + Rogan Josh', text: 'seekh kebab starter and rogan josh main' }, ]; @@ -78,11 +54,118 @@ export const VoiceAssistantModal: React.FC = ({ open, onClose, onAddToCart, + onSyncCart, }) => { - const [internalVoiceState, setInternalVoiceState] = useState('idle'); - const [detectedItems, setDetectedItems] = useState([]); + const [cartLines, setCartLines] = useState([]); + const [assistantText, setAssistantText] = useState(''); + const [toolCalls, setToolCalls] = useState([]); const [confirmed, setConfirmed] = useState(false); + const [busy, setBusy] = useState(false); + const busyRef = useRef(false); + const [apiError, setApiError] = useState(null); const [selectedLang, setSelectedLang] = useState<'en-IN' | 'hi-IN'>('en-IN'); + const [liveTranscript, setLiveTranscript] = useState(''); + const [typedOrder, setTypedOrder] = useState(''); + + const applyCart = useCallback( + (cart: AiCartLine[]) => { + setCartLines(cart); + if (onSyncCart) { + onSyncCart(cart); + return; + } + for (const line of cart) { + onAddToCart(String(line.menu_item_id), line.quantity); + } + }, + [onAddToCart, onSyncCart] + ); + + const runAiTurn = useCallback( + async (text: string, submit = false) => { + const trimmed = text.trim(); + if (!trimmed) { + debugLog.warn('voice', 'runAiTurn skipped — empty text'); + setApiError('Say or type an order first.'); + return; + } + if (busyRef.current) { + debugLog.warn('voice', 'runAiTurn skipped — already busy', { trimmed }); + return; + } + + const sessionToken = sessionStorage.getItem('customer_session_token'); + if (!sessionToken) { + debugLog.error('voice', 'runAiTurn blocked — no session token'); + setApiError('No table session — close this, pick a table, then Start ordering.'); + return; + } + + busyRef.current = true; + setBusy(true); + setApiError(null); + setAssistantText(''); + setToolCalls([]); + setConfirmed(false); + setLiveTranscript(trimmed); + + const looksHindi = /[\u0900-\u097F]/.test(trimmed); + const language = looksHindi || selectedLang.startsWith('hi') ? 'hi' : 'en'; + const started = performance.now(); + debugLog.info('voice', 'runAiTurn start', { + submit, + language, + selectedLang, + transcript: trimmed.slice(0, 160), + sessionToken: maskToken(sessionToken), + tableId: sessionStorage.getItem('customer_table_id'), + tableNumber: sessionStorage.getItem('customer_table_number'), + }); + + try { + const result = await api.aiChatTurn({ transcript: trimmed, language, submit }); + + debugLog.info('voice', 'runAiTurn success', { + ms: Math.round(performance.now() - started), + tools: result.tool_calls, + cart: (result.cart || []).map((c) => `${c.quantity}×${c.name}`), + orderId: result.order?.order_id, + assistant: (result.assistant_text || '').slice(0, 120), + }); + + setLiveTranscript(result.transcript || trimmed); + setAssistantText(result.assistant_text || ''); + setToolCalls(result.tool_calls || []); + if (result.order) { + setConfirmed(true); + applyCart([]); + } else { + applyCart(result.cart || []); + } + } catch (err: unknown) { + const raw = err instanceof Error ? err.message : 'AI ordering failed'; + const message = + raw.length > 160 || raw.includes('failed_generation') || raw.includes('tool_use_failed') + ? 'AI ordering failed — please try again or use the menu.' + : raw; + debugLog.error('voice', 'runAiTurn failed', { + ms: Math.round(performance.now() - started), + raw: String(raw).slice(0, 300), + shown: message, + }); + setApiError(message); + } finally { + busyRef.current = false; + setBusy(false); + } + }, + [applyCart, selectedLang] + ); + + const runAiTurnRef = useRef(runAiTurn); + useEffect(() => { + runAiTurnRef.current = runAiTurn; + }, [runAiTurn]); const { voiceState: recognitionState, @@ -93,101 +176,106 @@ export const VoiceAssistantModal: React.FC = ({ stopListening, resetTranscript, error, - } = useSpeechRecognition(); + } = useSpeechRecognition((finalText) => { + debugLog.info('stt', 'final transcript → AI', { text: finalText.slice(0, 160) }); + void runAiTurnRef.current(finalText, false); + }); - const { speak, cancel } = useSpeechSynthesis((s) => setInternalVoiceState(s)); - - const voiceState = internalVoiceState !== 'idle' ? internalVoiceState : recognitionState; - - // Parse items when transcript updates useEffect(() => { - if (transcript.trim().length > 0 && recognitionState === 'thinking') { - const items = parseTranscriptToItems(transcript); - setDetectedItems(items); - setInternalVoiceState('thinking'); - - // AI confirmation response via TTS - if (items.length > 0) { - const itemList = items.map((d) => `${d.qty} ${d.menuItem.name}`).join(', '); - setTimeout(() => { - speak( - `Got it! I found ${items.length} item${items.length > 1 ? 's' : ''}: ${itemList}. Please confirm to add them to your cart.`, - selectedLang, - () => setInternalVoiceState('idle') - ); - }, 500); - } else { - setTimeout(() => { - speak( - "Sorry, I couldn't identify any items from your order. Please try again or use the quick commands below.", - selectedLang, - () => setInternalVoiceState('idle') - ); - setInternalVoiceState('idle'); - }, 400); - } - } - }, [transcript, recognitionState]); - - const handleClose = useCallback(() => { - cancel(); - resetTranscript(); - setDetectedItems([]); + if (!open) return; + debugLog.info('voice', 'modal opened', { + isSupported, + selectedLang, + hasSession: Boolean(sessionStorage.getItem('customer_session_token')), + tableId: sessionStorage.getItem('customer_table_id'), + tableNumber: sessionStorage.getItem('customer_table_number'), + debug: debugLog.enabled(), + }); + busyRef.current = false; + setBusy(false); + setApiError(null); + setAssistantText(''); + setToolCalls([]); setConfirmed(false); - setInternalVoiceState('idle'); + setLiveTranscript(''); + setTypedOrder(''); + setCartLines([]); + resetTranscript(); + }, [open, resetTranscript, isSupported, selectedLang]); + + const voiceState: VoiceState = busy + ? 'thinking' + : recognitionState === 'listening' + ? 'listening' + : 'idle'; + + const handleClose = () => { + stopListening(); + resetTranscript(); + busyRef.current = false; + setBusy(false); onClose(); - }, [cancel, resetTranscript, onClose]); + }; const handleMicToggle = () => { - if (voiceState === 'listening') { - stopListening(); - } else { - setDetectedItems([]); - setConfirmed(false); - resetTranscript(); - startListening(selectedLang); + if (busyRef.current) { + debugLog.warn('stt', 'mic toggle ignored — AI busy'); + return; } + if (!isSupported) { + debugLog.warn('stt', 'mic unsupported'); + setApiError('Speech recognition is unavailable here. Type your order or tap a quick command.'); + return; + } + if (recognitionState === 'listening') { + debugLog.info('stt', 'mic stop requested'); + stopListening(); + return; + } + debugLog.info('stt', 'mic start', { lang: selectedLang }); + setApiError(null); + resetTranscript(); + startListening(selectedLang); }; const handleQuickCommand = (text: string) => { - const items = parseTranscriptToItems(text); - setDetectedItems(items); - setInternalVoiceState('thinking'); - if (items.length > 0) { - const itemList = items.map((d) => `${d.qty} ${d.menuItem.name}`).join(', '); - speak( - `Great choice! Found ${itemList}. Tap confirm to add to your cart.`, - selectedLang, - () => setInternalVoiceState('idle') - ); - } + debugLog.info('voice', 'quick command', { text: text.slice(0, 120) }); + void runAiTurn(text, false); + }; + + const handleTypedSubmit = () => { + debugLog.info('voice', 'typed submit', { text: typedOrder.slice(0, 120) }); + void runAiTurn(typedOrder, false); }; const handleConfirm = () => { - detectedItems.forEach((d) => onAddToCart(d.menuItem.id, d.qty)); - setConfirmed(true); - speak('Items added to your cart! Enjoy your meal.', selectedLang); - setTimeout(() => { - handleClose(); - }, 2000); + const text = + liveTranscript.trim() || + 'Please place my current cart as an order. I confirm submit.'; + void runAiTurn(text, true); }; const orbColor = { idle: { bg: 'linear-gradient(135deg, #ac2d00, #d53e0b)', shadow: '0 8px 24px rgba(172,45,0,0.4)' }, - listening: { bg: 'linear-gradient(135deg, #d53e0b, #e75a2b)', shadow: '0 0 0 16px rgba(213,62,11,0.2), 0 0 0 32px rgba(172,45,0,0.1)' }, + listening: { + bg: 'linear-gradient(135deg, #d53e0b, #e75a2b)', + shadow: '0 0 0 16px rgba(213,62,11,0.2), 0 0 0 32px rgba(172,45,0,0.1)', + }, thinking: { bg: 'linear-gradient(135deg, #845000, #b07000)', shadow: '0 8px 24px rgba(132,80,0,0.5)' }, - speaking: { bg: 'linear-gradient(135deg, #006a2e, #009c44)', shadow: '0 0 0 16px rgba(0,106,46,0.2), 0 0 0 32px rgba(0,106,46,0.1)' }, + speaking: { bg: 'linear-gradient(135deg, #006a2e, #009c44)', shadow: '0 8px 24px rgba(0,106,46,0.4)' }, error: { bg: 'linear-gradient(135deg, #ba1a1a, #ff5449)', shadow: '0 8px 24px rgba(186,26,26,0.5)' }, }; const orbLabel = { - idle: 'Tap to Speak', - listening: '🎙️ Listening...', - thinking: '🤔 Processing...', - speaking: '🔊 AI Speaking...', - error: '⚠️ Try Again', + idle: isSupported ? 'Tap to Speak' : 'Type or use quick commands', + listening: 'Listening… tap again to stop', + thinking: 'AI thinking…', + speaking: 'Done', + error: 'Try Again', }; + const shownTranscript = liveTranscript || transcript || interimTranscript; + return ( = ({ }} > - {/* Handle Bar */} - {/* Header */} @@ -219,7 +305,7 @@ export const VoiceAssistantModal: React.FC = ({ AI Voice Assistant - Speak your order naturally in English or Hindi + Type, tap a quick command, or speak → Groq fills your cart @@ -228,51 +314,52 @@ export const VoiceAssistantModal: React.FC = ({ - {/* Scrollable Body */} {!isSupported && ( - Voice recognition requires Google Chrome browser. You can still use quick commands below. + Speech recognition API is missing in this browser. Type your order or use a quick command. )} - {error && ( + {isSupported && ( + + Mic tip (Chrome): tap the orange button, speak, then tap again (or Stop) to send the order to AI. + + )} + + {(error || apiError) && ( - {error} + {apiError || error} )} - {/* Voice Orb */} - + - {voiceState === 'listening' ? ( - - ) : voiceState === 'speaking' ? ( - - ) : voiceState === 'thinking' ? ( - + {busy ? ( + + ) : voiceState === 'listening' ? ( + ) : ( - + )} - + {orbLabel[voiceState]} @@ -281,22 +368,21 @@ export const VoiceAssistantModal: React.FC = ({ variant="outlined" size="small" startIcon={} - onClick={stopListening} - sx={{ fontWeight: 700, color: '#ac2d00', borderColor: '#ac2d00' }} + onClick={handleMicToggle} + sx={{ fontWeight: 700, color: '#ac2d00', borderColor: '#ac2d00', mb: 1 }} > Stop & Process )} - {/* Language Toggle */} - - {['en-IN', 'hi-IN'].map((lang) => ( + + {(['en-IN', 'hi-IN'] as const).map((lang) => ( setSelectedLang(lang as 'en-IN' | 'hi-IN')} + onClick={() => setSelectedLang(lang)} color={selectedLang === lang ? 'primary' : 'default'} variant={selectedLang === lang ? 'filled' : 'outlined'} sx={{ fontWeight: 700 }} @@ -305,8 +391,29 @@ export const VoiceAssistantModal: React.FC = ({ - {/* Live Transcript */} - {(transcript || interimTranscript) && ( + + setTypedOrder(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') handleTypedSubmit(); + }} + /> + + + + {shownTranscript && ( = ({ }} > - WHAT I HEARD: + ORDER TEXT: - "{transcript}{interimTranscript && {interimTranscript}}" + "{shownTranscript}" )} - {/* Detected Items */} - 0}> + {assistantText && ( + + {assistantText} + + )} + + {toolCalls.length > 0 && ( + + Tools: {toolCalls.join(' → ')} + + )} + + 0}> - {detectedItems.length > 0 && ( + {cartLines.length > 0 && ( = ({ }} > - ✅ Items Detected: + Cart from AI: - {detectedItems.map((d) => ( + {cartLines.map((line) => ( = ({ > - {d.qty}× {d.menuItem.name} + {line.quantity}× {line.name} - ₹{(d.qty * d.menuItem.price).toFixed(0)} + ₹{(Number(line.unit_price) * line.quantity).toFixed(0)} + {line.notes?.length ? ` · ${line.notes.join(', ')}` : ''} @@ -375,24 +495,27 @@ export const VoiceAssistantModal: React.FC = ({ fullWidth variant="outlined" size="small" - onClick={() => { setDetectedItems([]); resetTranscript(); }} + onClick={() => { + setCartLines([]); + setAssistantText(''); + setLiveTranscript(''); + setToolCalls([]); + onSyncCart?.([]); + }} sx={{ fontWeight: 700 }} > - Retry + Clear @@ -400,7 +523,6 @@ export const VoiceAssistantModal: React.FC = ({ - {/* Quick Commands */} QUICK COMMANDS @@ -408,9 +530,9 @@ export const VoiceAssistantModal: React.FC = ({ - {QUICK_COMMANDS.map((cmd, idx) => ( + {QUICK_COMMANDS.map((cmd) => ( handleQuickCommand(cmd.text)} sx={{ @@ -418,18 +540,20 @@ export const VoiceAssistantModal: React.FC = ({ py: 1.5, borderRadius: '12px', border: '1px solid #e4beb4', - cursor: 'pointer', + cursor: busy ? 'wait' : 'pointer', display: 'flex', alignItems: 'center', gap: 1.5, + opacity: busy ? 0.6 : 1, + pointerEvents: busy ? 'none' : 'auto', '&:hover': { bgcolor: '#ffdbd1', borderColor: '#ac2d00' }, - transition: 'all 0.15s ease', }} > "{cmd.label}" + ))} diff --git a/src/components/kds/KDSKanban.tsx b/src/components/kds/KDSKanban.tsx index 8564ed1..9f6d741 100644 --- a/src/components/kds/KDSKanban.tsx +++ b/src/components/kds/KDSKanban.tsx @@ -18,6 +18,7 @@ import { KDSTicketCard } from './KDSTicketCard'; interface KDSKanbanProps { orders: KDSOrder[]; + busyOrderId?: string | null; onStatusChange: (id: string, newStatus: OrderStatus) => void; onToggleItem: (orderId: string, itemId: string) => void; onAddSampleOrder: () => void; @@ -25,6 +26,7 @@ interface KDSKanbanProps { export const KDSKanban: React.FC = ({ orders, + busyOrderId = null, onStatusChange, onToggleItem, onAddSampleOrder, @@ -50,26 +52,38 @@ export const KDSKanban: React.FC = ({ ]; return ( - + {/* Top Filter Bar */} - - - - KDS Kanban Board + + + + KDS Board @@ -124,19 +138,22 @@ export const KDSKanban: React.FC = ({ {/* 4 Column Kanban Board Grid */} - + {columns.map((col) => { const colOrders = filteredOrders.filter((o) => o.status === col.status); return ( - + {/* Column Header */} @@ -145,12 +162,23 @@ export const KDSKanban: React.FC = ({ display: 'flex', alignItems: 'center', justifyContent: 'space-between', - mb: 2, + gap: 1, + mb: 1.5, pb: 1, borderBottom: '2px solid #e2e2e2', + flexShrink: 0, }} > - + {col.title} = ({ {/* Column Tickets */} - + {colOrders.length === 0 ? ( @@ -186,6 +214,7 @@ export const KDSKanban: React.FC = ({ diff --git a/src/components/kds/KDSTicketCard.tsx b/src/components/kds/KDSTicketCard.tsx index 2335374..9e84423 100644 --- a/src/components/kds/KDSTicketCard.tsx +++ b/src/components/kds/KDSTicketCard.tsx @@ -11,6 +11,7 @@ import { Chip, IconButton, Tooltip, + CircularProgress, } from '@mui/material'; import AccessTimeIcon from '@mui/icons-material/AccessTime'; import ArrowForwardIcon from '@mui/icons-material/ArrowForward'; @@ -22,16 +23,17 @@ import { StatusBadge } from '../common/StatusBadge'; interface KDSTicketCardProps { order: KDSOrder; + busy?: boolean; onStatusChange: (id: string, newStatus: OrderStatus) => void; onToggleItem: (orderId: string, itemId: string) => void; } export const KDSTicketCard: React.FC = ({ order, + busy = false, onStatusChange, onToggleItem, }) => { - // Calculate SLA timer color const isUrgent = order.timeElapsedMinutes >= 25 || order.priority === 'urgent'; const isWarning = order.timeElapsedMinutes >= 15 && order.timeElapsedMinutes < 25; @@ -62,34 +64,56 @@ export const KDSTicketCard: React.FC = ({ served: 'COMPLETED', }[order.status]; - const allItemsChecked = order.items.every((i) => i.completed); + const allItemsChecked = order.items.length > 0 && order.items.every((i) => i.completed); + + const locationLabel = order.tableNumber + ? `Table ${order.tableNumber}` + : order.customerName || + (order.orderType === 'Takeaway' + ? 'Takeaway' + : order.orderType === 'Delivery' + ? 'Delivery' + : 'Walk-in'); return ( - {/* Ticket Header */} - - - + + + #{order.ticketNumber} @@ -103,79 +127,102 @@ export const KDSTicketCard: React.FC = ({ /> )} - - {order.tableNumber ? `Table ${order.tableNumber}` : order.customerName || 'Walk-in Guest'} - {order.serverName && ` • Server: ${order.serverName}`} + + {locationLabel} + {order.serverName ? ` · ${order.serverName}` : ''} - {/* SLA Timer Badge */} - - + + {order.timeElapsedMinutes}m - {/* Ticket Body / Items */} - - + + {order.items.map((item) => ( onToggleItem(order.id, item.id)} + onClick={() => !busy && onToggleItem(order.id, item.id)} sx={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', - p: 0.8, + gap: 1, + p: 0.75, borderRadius: '6px', - cursor: 'pointer', + cursor: busy ? 'default' : 'pointer', backgroundColor: item.completed ? '#f0eded' : 'transparent', - textDecoration: item.completed ? 'line-through' : 'none', - opacity: item.completed ? 0.6 : 1, - '&:hover': { - backgroundColor: '#f6f3f2', - }, + opacity: item.completed ? 0.65 : 1, + '&:hover': busy ? undefined : { backgroundColor: '#f6f3f2' }, }} > - + - - + + - {item.quantity}x + {item.quantity}× - + {item.name} - {item.dietary && } + {item.dietary && ( + + + + )} - {item.notes && ( = ({ color: '#ba1a1a', fontWeight: 700, display: 'block', - mt: 0.2, + mt: 0.35, backgroundColor: '#ffdad6', px: 0.8, py: 0.2, borderRadius: '4px', }} > - ⚠️ {item.notes} + {item.notes} )} - - ${(item.price * item.quantity).toFixed(2)} + + ₹{(item.price * item.quantity).toFixed(0)} ))} @@ -206,14 +256,22 @@ export const KDSTicketCard: React.FC = ({ - {/* Ticket Footer Actions */} {prevStatus && ( - - onStatusChange(order.id, prevStatus)}> - - + + + { + e.stopPropagation(); + onStatusChange(order.id, prevStatus); + }} + > + + + )} @@ -222,12 +280,18 @@ export const KDSTicketCard: React.FC = ({ ) : ( void; + onAddOrderFromVoice?: (transcript: string) => void; } -export const VoiceOrderingView: React.FC = ({ onAddOrderFromVoice }) => { - const [isListening, setIsListening] = useState(false); - const [transcript, setTranscript] = useState( - 'Do Butter Chicken, teen Garlic Naan aur ek Mango Lassi Table 12 ke liye' - ); - const [detectedItems] = useState([ - { name: 'Butter Chicken', qty: 2, price: 360, dietary: 'non-veg' as const }, - { name: 'Garlic Naan', qty: 3, price: 80, dietary: 'veg' as const }, - { name: 'Mango Lassi', qty: 1, price: 120, dietary: 'veg' as const }, - ]); - const [submitted, setSubmitted] = useState(false); +const QUICK_PROMPTS = [ + { label: 'What is low stock?', text: 'What ingredients are low on stock right now?' }, + { label: 'Add 2kg tomato', text: 'please add 2 kgs tomato in inventory' }, + { label: 'Chicken stock?', text: 'How much chicken do we have left?' }, + { label: 'Restock cream 1L', text: 'add 1 litre cream to inventory' }, + { label: 'Send alert now', text: 'Send a low stock alert notification now' }, + { label: 'Full inventory', text: 'Give me a quick inventory stock overview' }, +]; - const samplePrompts: VoiceCommandSuggestion[] = [ - { - id: '1', - label: '2 Butter Chicken & Garlic Naan – Table 12', - prompt: 'Do Butter Chicken aur do Garlic Naan Table 12 ke liye', - category: 'order', - }, - { - id: '2', - label: 'Hyderabadi Biryani + Raita – Table 8', - prompt: 'Ek Hyderabadi Chicken Biryani with extra raita for Table 8', - category: 'order', - }, - { - id: '3', - label: 'Jain Palak Paneer – no onion no garlic', - prompt: 'Add 1 Palak Paneer Jain preparation no onion no garlic for Table 4', - category: 'order', - }, - { - id: '4', - label: 'Extra Cutlery + Tissue for Table 3', - prompt: 'Please send extra cutlery and tissue paper to Table 3', - category: 'action', - }, - ]; +export const VoiceOrderingView: React.FC = () => { + const [typed, setTyped] = useState(''); + const [liveTranscript, setLiveTranscript] = useState(''); + const [assistantText, setAssistantText] = useState(''); + const [toolCalls, setToolCalls] = useState([]); + const [lowStock, setLowStock] = useState([]); + const [updates, setUpdates] = useState([]); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(null); + const [selectedLang, setSelectedLang] = useState<'en-IN' | 'hi-IN'>('en-IN'); + const busyRef = useRef(false); - const handleConfirmOrder = () => { - onAddOrderFromVoice(transcript); - setSubmitted(true); - setTimeout(() => setSubmitted(false), 3000); + const runStaffAi = useCallback(async (text: string) => { + const trimmed = text.trim(); + if (!trimmed || busyRef.current) return; + busyRef.current = true; + setBusy(true); + setError(null); + setLiveTranscript(trimmed); + setAssistantText(''); + try { + const language = selectedLang.startsWith('hi') || /[\u0900-\u097F]/.test(trimmed) ? 'hi' : 'en'; + const result = await api.staffAiChatTurn({ transcript: trimmed, language }); + setAssistantText(result.assistant_text || ''); + setToolCalls(result.tool_calls || []); + setLowStock(result.low_stock || []); + setUpdates(result.updates || []); + setLiveTranscript(result.transcript || trimmed); + } catch (err) { + setError(err instanceof Error ? err.message : 'Staff AI failed'); + } finally { + busyRef.current = false; + setBusy(false); + } + }, [selectedLang]); + + const runStaffAiRef = useRef(runStaffAi); + useEffect(() => { + runStaffAiRef.current = runStaffAi; + }, [runStaffAi]); + + const { + voiceState, + transcript, + interimTranscript, + isSupported, + startListening, + stopListening, + resetTranscript, + error: sttError, + } = useSpeechRecognition((finalText) => { + void runStaffAiRef.current(finalText); + }); + + useEffect(() => { + // Prefetch low stock so the panel isn't empty on first open. + void api + .getLowStockInventory() + .then((rows) => { + const list = Array.isArray(rows) ? rows : rows?.items || []; + setLowStock( + list.map((r: any) => ({ + id: Number(r.id), + name: String(r.name), + unit: String(r.unit || ''), + current_stock: Number(r.current_stock ?? r.stock ?? 0), + reorder_threshold: Number(r.reorder_threshold ?? r.minStock ?? 0), + is_low: true, + })), + ); + }) + .catch(() => { + /* ignore prefetch errors */ + }); + }, []); + + const toggleMic = () => { + if (voiceState === 'listening') { + stopListening(); + const spoken = (transcript || interimTranscript || '').trim(); + if (spoken) void runStaffAi(spoken); + return; + } + resetTranscript(); + setLiveTranscript(''); + startListening(selectedLang); }; - const totalValue = detectedItems.reduce((sum, i) => sum + i.price * i.qty, 0); + const shownTranscript = liveTranscript || transcript || interimTranscript; + const listening = voiceState === 'listening'; return ( - - + + - Staff Voice Assistant Terminal + Staff AI Assistant - Hands-free voice intake for servers & kitchen staff. Supports Hindi, English & Hinglish. + Ask about inventory by voice or text — get low-stock checks and send alerts anytime. - {/* Voice Orb */} - - setIsListening(!isListening)} - sx={{ - width: 130, - height: 130, - borderRadius: '50%', - mx: 'auto', - mb: 3, - cursor: 'pointer', - background: isListening - ? 'radial-gradient(circle, #d53e0b 0%, #ac2d00 70%)' - : 'radial-gradient(circle, #ac2d00 0%, #872100 100%)', - boxShadow: isListening - ? '0 0 0 18px rgba(213, 62, 11, 0.2), 0 0 0 36px rgba(172, 45, 0, 0.1)' - : '0 4px 20px rgba(172, 45, 0, 0.3)', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - color: '#ffffff', - transition: 'all 0.3s ease-in-out', - transform: isListening ? 'scale(1.08)' : 'scale(1)', - }} - > - {isListening ? : } - - - - {isListening ? '🎙️ Listening & Parsing Voice Input...' : 'Tap Orb or Speak to Begin Order'} - - - Multilingual: English · Hindi · Hinglish - - - {/* Live Transcript */} - - - LIVE TRANSCRIPT: - - - "{transcript}" - - - - - {/* Quick Prompts */} - - - Suggested Quick Voice Commands - - - {samplePrompts.map((s) => ( - - } - label={s.label} - onClick={() => { setTranscript(s.prompt); setIsListening(true); setTimeout(() => setIsListening(false), 1500); }} - clickable - sx={{ width: '100%', justifyContent: 'flex-start', py: 2.5, px: 1, fontWeight: 700, bgcolor: '#ffffff', border: '1px solid #e4beb4', '&:hover': { bgcolor: '#ffdbd1' } }} - /> - - ))} - - - - {/* Detected Items */} - - - AI Extracted Items - - - - - {detectedItems.map((item, idx) => ( - + + + - - - {item.qty}× + {busy ? : listening ? : } + + + {busy ? 'AI checking inventory…' : listening ? 'Listening… tap to stop' : isSupported ? 'Tap to speak' : 'Type a question below'} + + + {(['en-IN', 'hi-IN'] as const).map((lang) => ( + setSelectedLang(lang)} + color={selectedLang === lang ? 'primary' : 'default'} + sx={{ fontWeight: 700 }} + /> + ))} + + {(error || sttError) && ( + + {error || sttError} + + )} + {shownTranscript && ( + + + You said - {item.name}} - secondary={`Subtotal: ₹${(item.qty * item.price)}`} + {shownTranscript} + + )} + + + + + + + + AI reply + + {assistantText ? ( + } + sx={{ fontWeight: 600, mb: 1.5 }} + > + {assistantText} + + ) : ( + + Try: “Add 2 kg tomato” or “What is low stock?” + + )} + {updates.length > 0 && ( + + {updates.map((row) => ( + + ))} + + )} + {toolCalls.length > 0 && ( + + Tools: {toolCalls.join(' → ')} + + )} + + + + + setTyped(e.target.value)} + onKeyDown={(e) => { + if (e.key === 'Enter' && typed.trim()) { + void runStaffAi(typed); + setTyped(''); + } + }} + sx={{ '& .MuiOutlinedInput-root': { borderRadius: '12px' } }} + /> + + + + + {QUICK_PROMPTS.map((p) => ( + void runStaffAi(p.text)} + sx={{ fontWeight: 700 }} + /> + ))} + + + + + + + + Low stock now + - - - ))} - - - - - Total: ₹{totalValue} - - - - - - + + + {lowStock.length === 0 ? ( + + No ingredients below reorder threshold. + + ) : ( + + {lowStock.map((row) => ( + + {row.name} + } + secondary={`Now ${row.current_stock}${row.unit} · reorder at ${row.reorder_threshold}${row.unit}`} + /> + + + ))} + + )} + + + ); }; diff --git a/src/data/menuData.ts b/src/data/menuData.ts index 2569623..672d06a 100644 --- a/src/data/menuData.ts +++ b/src/data/menuData.ts @@ -1,5 +1,5 @@ export interface MenuItem { - id: string; + id: number; name: string; nameHindi?: string; category: 'Starters' | 'Biryani' | 'Mains' | 'Breads' | 'Drinks' | 'Desserts' | 'Specials'; @@ -12,12 +12,14 @@ export interface MenuItem { image: string; spiceLevel?: 'mild' | 'medium' | 'hot' | 'extra-hot'; tags?: string[]; + /** Present when loaded from API; static fallback treats missing as available */ + isAvailable?: boolean; } export const MENU_ITEMS: MenuItem[] = [ // ─── STARTERS ─────────────────────────────────────────────────── { - id: 'm1', + id: 1, name: 'Samosa Chaat', nameHindi: 'समोसा चाट', category: 'Starters', @@ -31,7 +33,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Popular', 'Street Food'], }, { - id: 'm2', + id: 2, name: 'Paneer Tikka', nameHindi: 'पनीर टिक्का', category: 'Starters', @@ -45,7 +47,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ["Chef's Special", 'Tandoor'], }, { - id: 'm3', + id: 3, name: 'Chicken 65', nameHindi: 'चिकन 65', category: 'Starters', @@ -59,7 +61,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['South Indian', 'Crispy'], }, { - id: 'm4', + id: 4, name: 'Hara Bhara Kabab', nameHindi: 'हरा भरा कबाब', category: 'Starters', @@ -72,7 +74,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Healthy', 'Spinach'], }, { - id: 'm5', + id: 5, name: 'Seekh Kebab', nameHindi: 'सीख कबाब', category: 'Starters', @@ -88,7 +90,7 @@ export const MENU_ITEMS: MenuItem[] = [ // ─── BIRYANI ──────────────────────────────────────────────────── { - id: 'm6', + id: 6, name: 'Hyderabadi Chicken Biryani', nameHindi: 'हैदराबादी चिकन बिरयानी', category: 'Biryani', @@ -103,7 +105,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Dum Style', 'Hyderabadi', 'Bestseller'], }, { - id: 'm7', + id: 7, name: 'Veg Dum Biryani', nameHindi: 'वेज दम बिरयानी', category: 'Biryani', @@ -116,7 +118,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Dum Style', 'Veg'], }, { - id: 'm8', + id: 8, name: 'Mutton Biryani', nameHindi: 'मटन बिरयानी', category: 'Biryani', @@ -131,7 +133,7 @@ export const MENU_ITEMS: MenuItem[] = [ // ─── MAINS ────────────────────────────────────────────────────── { - id: 'm9', + id: 9, name: 'Butter Chicken', nameHindi: 'बटर चिकन', category: 'Mains', @@ -145,7 +147,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Classic', 'Bestseller'], }, { - id: 'm10', + id: 10, name: 'Dal Makhani', nameHindi: 'दाल मखनी', category: 'Mains', @@ -159,7 +161,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Punjabi', 'Classic'], }, { - id: 'm11', + id: 11, name: 'Palak Paneer', nameHindi: 'पालक पनीर', category: 'Mains', @@ -172,7 +174,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Healthy', 'Jain Available'], }, { - id: 'm12', + id: 12, name: 'Chicken Kadhai', nameHindi: 'चिकन कड़ाही', category: 'Mains', @@ -186,7 +188,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Kadhai Style', "Chef's Special"], }, { - id: 'm13', + id: 13, name: 'Paneer Butter Masala', nameHindi: 'पनीर बटर मसाला', category: 'Mains', @@ -199,7 +201,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Popular'], }, { - id: 'm14', + id: 14, name: 'Rogan Josh', nameHindi: 'रोगन जोश', category: 'Mains', @@ -214,7 +216,7 @@ export const MENU_ITEMS: MenuItem[] = [ // ─── BREADS ────────────────────────────────────────────────────── { - id: 'm15', + id: 15, name: 'Butter Naan', nameHindi: 'बटर नान', category: 'Breads', @@ -226,7 +228,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Tandoor'], }, { - id: 'm16', + id: 16, name: 'Garlic Naan', nameHindi: 'लहसुन नान', category: 'Breads', @@ -239,7 +241,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Bestseller', 'Tandoor'], }, { - id: 'm17', + id: 17, name: 'Lachha Paratha', nameHindi: 'लच्छा पराठा', category: 'Breads', @@ -253,7 +255,7 @@ export const MENU_ITEMS: MenuItem[] = [ // ─── DRINKS ────────────────────────────────────────────────────── { - id: 'm18', + id: 18, name: 'Mango Lassi', nameHindi: 'आम की लस्सी', category: 'Drinks', @@ -266,7 +268,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Summer Special', 'Fresh'], }, { - id: 'm19', + id: 19, name: 'Masala Chai', nameHindi: 'मसाला चाय', category: 'Drinks', @@ -278,7 +280,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Kulhad', 'Hot'], }, { - id: 'm20', + id: 20, name: 'Sweet Lime Soda', nameHindi: 'नींबू सोडा', category: 'Drinks', @@ -290,7 +292,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Fresh', 'Refreshing'], }, { - id: 'm21', + id: 21, name: 'Rose Sharbat', nameHindi: 'गुलाब शरबत', category: 'Drinks', @@ -304,7 +306,7 @@ export const MENU_ITEMS: MenuItem[] = [ // ─── DESSERTS ──────────────────────────────────────────────────── { - id: 'm22', + id: 22, name: 'Gulab Jamun', nameHindi: 'गुलाब जामुन', category: 'Desserts', @@ -317,7 +319,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Hot', 'Classic'], }, { - id: 'm23', + id: 23, name: 'Rasmalai', nameHindi: 'रसमलाई', category: 'Desserts', @@ -330,7 +332,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Bengali', "Chef's Special", 'Cold'], }, { - id: 'm24', + id: 24, name: 'Gajar Halwa', nameHindi: 'गाजर का हलवा', category: 'Desserts', @@ -344,7 +346,7 @@ export const MENU_ITEMS: MenuItem[] = [ // ─── SPECIALS ───────────────────────────────────────────────────── { - id: 'm25', + id: 25, name: 'Chef\'s Thali', nameHindi: 'शेफ की थाली', category: 'Specials', @@ -358,7 +360,7 @@ export const MENU_ITEMS: MenuItem[] = [ tags: ['Thali', 'Value', 'Complete Meal'], }, { - id: 'm26', + id: 26, name: 'Non-Veg Thali', nameHindi: 'नॉन-वेज थाली', category: 'Specials', diff --git a/src/hooks/useSpeechRecognition.ts b/src/hooks/useSpeechRecognition.ts index d5dec72..6206ec2 100644 --- a/src/hooks/useSpeechRecognition.ts +++ b/src/hooks/useSpeechRecognition.ts @@ -1,4 +1,5 @@ import { useState, useEffect, useRef, useCallback } from 'react'; +import { debugLog } from '../utils/debugLog'; interface SpeechRecognitionEvent extends Event { results: SpeechRecognitionResultList; @@ -43,12 +44,28 @@ interface UseSpeechRecognitionReturn { error: string | null; } -export const useSpeechRecognition = (): UseSpeechRecognitionReturn => { +type FinalHandler = (text: string) => void; + +/** + * Chrome Web Speech (webkitSpeechRecognition). + * Submits final text on end; if Chrome only left interim results, submits those too. + */ +export const useSpeechRecognition = ( + onFinalTranscript?: FinalHandler +): UseSpeechRecognitionReturn => { const [voiceState, setVoiceState] = useState('idle'); - const [transcript, setTranscript] = useState(''); - const [interimTranscript, setInterimTranscript] = useState(''); + const [transcript, setTranscript] = useState(''); + const [interimTranscript, setInterimTranscript] = useState(''); const [error, setError] = useState(null); const recognitionRef = useRef(null); + const finalBufferRef = useRef(''); + const interimRef = useRef(''); + const submittedRef = useRef(false); + const onFinalRef = useRef(onFinalTranscript); + + useEffect(() => { + onFinalRef.current = onFinalTranscript; + }, [onFinalTranscript]); const isSupported = typeof window !== 'undefined' && @@ -56,84 +73,160 @@ export const useSpeechRecognition = (): UseSpeechRecognitionReturn => { useEffect(() => { return () => { - recognitionRef.current?.abort(); + try { + recognitionRef.current?.abort(); + } catch { + /* ignore */ + } }; }, []); - const startListening = useCallback((lang: string = 'en-IN') => { - if (!isSupported) { - setError('Voice recognition is not supported in this browser. Please use Chrome.'); - setVoiceState('error'); + const emitFinal = useCallback(() => { + if (submittedRef.current) return; + const text = (finalBufferRef.current || interimRef.current).trim(); + if (!text) { + debugLog.warn('stt', 'emitFinal skipped — empty buffer'); return; } - - const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition; - const recognition = new SpeechRecognitionAPI(); - recognitionRef.current = recognition; - - recognition.lang = lang; - recognition.continuous = false; - recognition.interimResults = true; - recognition.maxAlternatives = 1; - - recognition.onstart = () => { - setVoiceState('listening'); - setError(null); - setInterimTranscript(''); - }; - - recognition.onresult = (event: SpeechRecognitionEvent) => { - let interim = ''; - let final = ''; - - for (let i = event.resultIndex; i < event.results.length; i++) { - const result = event.results[i]; - if (result.isFinal) { - final += result[0].transcript; - } else { - interim += result[0].transcript; - } - } - - setInterimTranscript(interim); - if (final) { - setTranscript((prev) => prev + ' ' + final.trim()); - setVoiceState('thinking'); - } - }; - - recognition.onerror = (event: SpeechRecognitionErrorEvent) => { - const errorMessages: Record = { - 'no-speech': 'No speech detected. Please try again.', - 'audio-capture': 'Microphone not accessible.', - 'not-allowed': 'Microphone permission denied. Please allow access.', - 'network': 'Network error. Check your internet connection.', - }; - setError(errorMessages[event.error] || `Voice error: ${event.error}`); - setVoiceState('error'); - }; - - recognition.onend = () => { - setInterimTranscript(''); - if (voiceState === 'listening') { - setVoiceState('thinking'); - } - }; - - try { - recognition.start(); - } catch (e) { - setError('Could not start voice recognition.'); - setVoiceState('error'); - } - }, [isSupported, voiceState]); - - const stopListening = useCallback(() => { - recognitionRef.current?.stop(); - setVoiceState('thinking'); + submittedRef.current = true; + setTranscript(text); + setInterimTranscript(''); + debugLog.info('stt', 'emitFinal', { + source: finalBufferRef.current.trim() ? 'final' : 'interim-fallback', + text: text.slice(0, 160), + }); + onFinalRef.current?.(text); }, []); + const startListening = useCallback( + (lang: string = 'en-IN') => { + if (!isSupported) { + setError('Voice recognition is not supported in this browser.'); + setVoiceState('error'); + return; + } + + try { + recognitionRef.current?.abort(); + } catch { + /* ignore */ + } + + finalBufferRef.current = ''; + interimRef.current = ''; + submittedRef.current = false; + setTranscript(''); + setInterimTranscript(''); + setError(null); + + const SpeechRecognitionAPI = window.SpeechRecognition || window.webkitSpeechRecognition; + const recognition = new SpeechRecognitionAPI(); + recognitionRef.current = recognition; + + // continuous=true: user taps stop; Chrome is more reliable this way than auto-end. + recognition.lang = lang === 'hi-IN' ? 'hi-IN' : 'en-IN'; + recognition.continuous = true; + recognition.interimResults = true; + recognition.maxAlternatives = 1; + + recognition.onstart = () => { + debugLog.info('stt', 'recognition onstart', { lang: recognition.lang }); + setVoiceState('listening'); + setError(null); + }; + + recognition.onresult = (event: SpeechRecognitionEvent) => { + let interim = ''; + let finals = finalBufferRef.current; + + for (let i = event.resultIndex; i < event.results.length; i++) { + const result = event.results[i]; + const piece = result[0]?.transcript || ''; + if (result.isFinal) { + finals = `${finals} ${piece}`.trim(); + } else { + interim += piece; + } + } + + finalBufferRef.current = finals; + interimRef.current = interim; + setTranscript(finals); + setInterimTranscript(interim); + debugLog.info('stt', 'recognition onresult', { + final: finals.slice(0, 120), + interim: interim.slice(0, 120), + }); + }; + + recognition.onerror = (event: SpeechRecognitionErrorEvent) => { + // Chrome fires "aborted" when we restart/stop — not a real failure. + if (event.error === 'aborted' || event.error === 'no-speech') { + debugLog.info('stt', 'recognition soft-error ignored', { error: event.error }); + return; + } + debugLog.error('stt', 'recognition onerror', { error: event.error }); + const errorMessages: Record = { + 'audio-capture': 'Microphone not accessible. Check Chrome site permissions.', + 'not-allowed': 'Microphone blocked. Allow mic for localhost in Chrome.', + network: + 'Chrome speech service network error. Use the text box or a quick command.', + 'service-not-allowed': 'Chrome speech service blocked. Use text / quick command.', + }; + setError(errorMessages[event.error] || `Voice error: ${event.error}`); + setVoiceState('error'); + }; + + recognition.onend = () => { + debugLog.info('stt', 'recognition onend', { + finalBuf: finalBufferRef.current.slice(0, 120), + interimBuf: interimRef.current.slice(0, 120), + submitted: submittedRef.current, + }); + setVoiceState('idle'); + emitFinal(); + }; + + try { + recognition.start(); + debugLog.info('stt', 'recognition.start() called', { lang: recognition.lang, continuous: true }); + } catch (err) { + debugLog.error('stt', 'recognition.start() threw', { + error: err instanceof Error ? err.message : String(err), + }); + setError('Could not start Chrome speech recognition. Try the text box.'); + setVoiceState('error'); + } + }, + [emitFinal, isSupported] + ); + + const stopListening = useCallback(() => { + const recognition = recognitionRef.current; + if (!recognition) { + emitFinal(); + setVoiceState('idle'); + return; + } + try { + recognition.stop(); + } catch { + emitFinal(); + setVoiceState('idle'); + } + // Chrome sometimes delays onend — don't leave the guest hanging. + window.setTimeout(() => { + if (!submittedRef.current) { + emitFinal(); + setVoiceState('idle'); + } + }, 600); + }, [emitFinal]); + const resetTranscript = useCallback(() => { + finalBufferRef.current = ''; + interimRef.current = ''; + submittedRef.current = false; setTranscript(''); setInterimTranscript(''); setVoiceState('idle'); diff --git a/src/hooks/useSpeechSynthesis.ts b/src/hooks/useSpeechSynthesis.ts index 54cdbd6..381dc3e 100644 --- a/src/hooks/useSpeechSynthesis.ts +++ b/src/hooks/useSpeechSynthesis.ts @@ -15,7 +15,10 @@ export const useSpeechSynthesis = ( const speak = useCallback( (text: string, lang: string = 'en-IN', onEnd?: () => void) => { - if (!isSupported) return; + if (!isSupported) { + onEnd?.(); + return; + } window.speechSynthesis.cancel(); const utterance = new SpeechSynthesisUtterance(text); @@ -24,20 +27,27 @@ export const useSpeechSynthesis = ( utterance.pitch = 1.05; utterance.volume = 1; - utterance.onstart = () => { - setVoiceState?.('speaking'); - }; - - utterance.onend = () => { + let settled = false; + const finish = () => { + if (settled) return; + settled = true; setVoiceState?.('idle'); onEnd?.(); }; - utterance.onerror = () => { - setVoiceState?.('idle'); + utterance.onstart = () => { + setVoiceState?.('speaking'); }; + utterance.onend = finish; + utterance.onerror = finish; - window.speechSynthesis.speak(utterance); + try { + window.speechSynthesis.speak(utterance); + // Firefox can leave speech pending without onend — unblock UI. + window.setTimeout(finish, 5000); + } catch { + finish(); + } }, [isSupported, setVoiceState] ); diff --git a/src/i18n/LocaleContext.tsx b/src/i18n/LocaleContext.tsx new file mode 100644 index 0000000..6d2f561 --- /dev/null +++ b/src/i18n/LocaleContext.tsx @@ -0,0 +1,59 @@ +import React, { createContext, useCallback, useContext, useMemo, useState } from 'react'; +import { + messages, + toAppLocale, + type AppLocale, + type MessageKey, +} from './messages'; + +const STORAGE_KEY = 'customer_locale'; + +type LocaleContextValue = { + locale: AppLocale; + setLocale: (locale: AppLocale) => void; + t: (key: MessageKey) => string; +}; + +const LocaleContext = createContext(null); + +function readStoredLocale(): AppLocale { + try { + return toAppLocale(sessionStorage.getItem(STORAGE_KEY)); + } catch { + return 'en'; + } +} + +export const LocaleProvider: React.FC<{ children: React.ReactNode; initial?: AppLocale }> = ({ + children, + initial, +}) => { + const [locale, setLocaleState] = useState(initial || readStoredLocale()); + + const setLocale = useCallback((next: AppLocale) => { + setLocaleState(next); + try { + sessionStorage.setItem(STORAGE_KEY, next); + document.documentElement.lang = next === 'hi' ? 'hi' : 'en'; + } catch { + /* ignore */ + } + }, []); + + const t = useCallback( + (key: MessageKey) => messages[locale][key] || messages.en[key] || key, + [locale], + ); + + const value = useMemo(() => ({ locale, setLocale, t }), [locale, setLocale, t]); + + return {children}; +}; + +export function useLocale(): LocaleContextValue { + const ctx = useContext(LocaleContext); + if (!ctx) { + throw new Error('useLocale must be used within LocaleProvider'); + } + return ctx; +} diff --git a/src/i18n/messages.ts b/src/i18n/messages.ts new file mode 100644 index 0000000..c059ef5 --- /dev/null +++ b/src/i18n/messages.ts @@ -0,0 +1,134 @@ +export type AppLocale = 'en' | 'hi' | 'hinglish'; + +export type MessageKey = + | 'brand' + | 'welcome' + | 'tableLabel' + | 'selectTable' + | 'tableLockedHint' + | 'tablePickHint' + | 'noTablesAvailable' + | 'chooseLanguage' + | 'startOrdering' + | 'voiceOrder' + | 'staffLogin' + | 'menu' + | 'cart' + | 'yourOrder' + | 'placeOrder' + | 'orderStatus' + | 'payBill' + | 'addToCart' + | 'back' + | 'loading' + | 'offlineHint' + | 'reconnectKds' + | 'kdsLive' + | 'logout'; + +const en: Record = { + brand: 'RestroAI', + welcome: 'Welcome to your table', + tableLabel: 'Table', + selectTable: 'Select table', + tableLockedHint: 'Table locked from QR scan', + tablePickHint: 'Tap to choose your table', + noTablesAvailable: 'No free tables right now — ask staff', + chooseLanguage: 'Choose language', + startOrdering: 'Start ordering', + voiceOrder: 'Order with voice', + staffLogin: 'Staff login', + menu: 'Menu', + cart: 'Cart', + yourOrder: 'Your order', + placeOrder: 'Place order', + orderStatus: 'Order status', + payBill: 'Pay bill', + addToCart: 'Add to cart', + back: 'Back', + loading: 'Loading…', + offlineHint: 'You are offline — showing cached menu when available.', + reconnectKds: 'Reconnecting to kitchen…', + kdsLive: 'Kitchen live', + logout: 'Log out', +}; + +const hi: Record = { + brand: 'RestroAI', + welcome: 'आपकी मेज़ पर स्वागत है', + tableLabel: 'टेबल', + selectTable: 'टेबल चुनें', + tableLockedHint: 'QR स्कैन से टेबल लॉक है', + tablePickHint: 'अपनी टेबल चुनने के लिए टैप करें', + noTablesAvailable: 'अभी कोई खाली टेबल नहीं — स्टाफ़ से पूछें', + chooseLanguage: 'भाषा चुनें', + startOrdering: 'ऑर्डर शुरू करें', + voiceOrder: 'आवाज़ से ऑर्डर', + staffLogin: 'स्टाफ़ लॉगिन', + menu: 'मेनू', + cart: 'कार्ट', + yourOrder: 'आपका ऑर्डर', + placeOrder: 'ऑर्डर भेजें', + orderStatus: 'ऑर्डर स्थिति', + payBill: 'बिल भुगतान', + addToCart: 'कार्ट में डालें', + back: 'वापस', + loading: 'लोड हो रहा है…', + offlineHint: 'आप ऑफ़लाइन हैं — कैश मेनू दिखाया जा रहा है।', + reconnectKds: 'किचन से फिर जुड़ रहे हैं…', + kdsLive: 'किचन लाइव', + logout: 'लॉग आउट', +}; + +/** Hinglish: Hindi structure with English restaurant terms. */ +const hinglish: Record = { + brand: 'RestroAI', + welcome: 'Aapki table par swagat hai', + tableLabel: 'Table', + selectTable: 'Table select karein', + tableLockedHint: 'QR scan se table lock hai', + tablePickHint: 'Apni table choose karne ke liye tap karein', + noTablesAvailable: 'Abhi free table nahi — staff se poochhein', + chooseLanguage: 'Language choose karein', + startOrdering: 'Ordering start karein', + voiceOrder: 'Voice se order', + staffLogin: 'Staff login', + menu: 'Menu', + cart: 'Cart', + yourOrder: 'Aapka order', + placeOrder: 'Order place karein', + orderStatus: 'Order status', + payBill: 'Bill pay karein', + addToCart: 'Cart mein add', + back: 'Back', + loading: 'Loading…', + offlineHint: 'Aap offline ho — cached menu dikha rahe hain.', + reconnectKds: 'Kitchen se reconnect ho raha hai…', + kdsLive: 'Kitchen live', + logout: 'Log out', +}; + +export const messages: Record> = { + en, + hi, + hinglish, +}; + +export function toAppLocale(raw: string | null | undefined): AppLocale { + const v = (raw || 'en').toLowerCase(); + if (v === 'hi' || v === 'hindi') return 'hi'; + if (v === 'hinglish') return 'hinglish'; + return 'en'; +} + +export function toSessionLanguage(locale: AppLocale): 'en' | 'hi' | 'hinglish' { + if (locale === 'hi') return 'hi'; + if (locale === 'hinglish') return 'hinglish'; + return 'en'; +} + +export function uiLangToLocale(ui: 'english' | 'hindi' | 'hinglish'): AppLocale { + if (ui === 'hindi') return 'hi'; + if (ui === 'hinglish') return 'hinglish'; + return 'en'; +} diff --git a/src/main.tsx b/src/main.tsx index bef5202..cd23b1c 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,10 +1,10 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './index.css'; +import { CustomerApp } from './apps/customer/App'; createRoot(document.getElementById('root')!).render( - + , -) +); diff --git a/src/services/api.ts b/src/services/api.ts index 20e4442..b0a0b34 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -1,44 +1,146 @@ -import { type MenuItem, MENU_ITEMS } from '../data/menuData'; +import { type MenuItem, MENU_ITEMS, CATEGORIES } from '../data/menuData'; +import type { CategoryType } from '../data/menuData'; +import { debugLog, maskToken } from '../utils/debugLog'; const BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'; const WS_BASE_URL = import.meta.env.VITE_WS_URL || 'ws://localhost:8000'; +const DEMO_FALLBACK = import.meta.env.VITE_DEMO_FALLBACK === 'true'; + +export type PublicMenuItem = { + id: number; + name: string; + name_hindi?: string | null; + description?: string | null; + price: number | string; + prep_time_minutes?: number | null; + dietary_type?: string | null; + image_url?: string | null; + spice_level?: string | null; + is_available: boolean; + is_chef_special: boolean; + is_bestseller: boolean; + tags: string[]; +}; + +export type PublicMenuCategory = { + id: number; + name: string; + display_order: number; + items: PublicMenuItem[]; +}; + +export type PublicMenuResponse = { + restaurant_id: number; + updated_at?: string | null; + categories: PublicMenuCategory[]; +}; + +export type StaffMenuItem = { + id: number; + restaurant_id: number; + category_id: number | null; + category_name?: string | null; + name: string; + name_hindi?: string | null; + description?: string | null; + price: number | string; + gst_rate: number | string; + prep_time_minutes?: number | null; + dietary_type?: string | null; + image_url?: string | null; + spice_level?: string | null; + is_available: boolean; + is_chef_special: boolean; + is_bestseller: boolean; + tags: { id: number; tag_type: string; value: string }[]; +}; + +export type StaffMenuCategory = { + id: number; + restaurant_id: number; + name: string; + display_order: number; +}; + +const toUiDietary = (value?: string | null): MenuItem['dietary'] => { + if (value === 'non_veg' || value === 'non-veg') return 'non-veg'; + if (value === 'vegan' || value === 'jain' || value === 'veg') return value; + return 'veg'; +}; + +export const flattenPublicMenu = (catalog: PublicMenuResponse): MenuItem[] => { + return catalog.categories.flatMap((category) => + category.items.map((item) => ({ + id: item.id, + name: item.name, + nameHindi: item.name_hindi || undefined, + category: (CATEGORIES.includes(category.name as CategoryType) + ? category.name + : 'Specials') as MenuItem['category'], + price: Number(item.price), + prepTimeMinutes: item.prep_time_minutes ?? 10, + dietary: toUiDietary(item.dietary_type), + isChefSpecial: item.is_chef_special, + isBestseller: item.is_bestseller, + description: item.description || '', + image: item.image_url || '', + spiceLevel: (item.spice_level as MenuItem['spiceLevel']) || undefined, + tags: item.tags || [], + isAvailable: item.is_available, + })) + ); +}; // Helpers to get/set auth tokens const getAccessToken = () => localStorage.getItem('access_token'); +export { getAccessToken }; export const getRefreshToken = () => localStorage.getItem('refresh_token'); const setTokens = (access: string, refresh: string) => { localStorage.setItem('access_token', access); localStorage.setItem('refresh_token', refresh); }; -const clearTokens = () => { +export const clearTokens = () => { localStorage.removeItem('access_token'); localStorage.removeItem('refresh_token'); }; const getCustomerToken = () => sessionStorage.getItem('customer_session_token'); const getCustomerSessionId = () => sessionStorage.getItem('customer_session_id'); -const setCustomerSession = (token: string, sessionId: string, tableId: string, restaurantId: string) => { +const setCustomerSession = ( + token: string, + sessionId: string, + tableId: string, + restaurantId: string, + tableNumber?: string, +) => { sessionStorage.setItem('customer_session_token', token); sessionStorage.setItem('customer_session_id', sessionId); sessionStorage.setItem('customer_table_id', tableId); sessionStorage.setItem('customer_restaurant_id', restaurantId); + if (tableNumber) { + sessionStorage.setItem('customer_table_number', tableNumber); + } }; export const clearCustomerSession = () => { sessionStorage.removeItem('customer_session_token'); sessionStorage.removeItem('customer_session_id'); sessionStorage.removeItem('customer_table_id'); sessionStorage.removeItem('customer_restaurant_id'); + sessionStorage.removeItem('customer_table_number'); }; -// Map front-end item ID (e.g. 'm1') to back-end numeric ID -export const getBackendMenuId = (frontendId: string): number => { - return parseInt(frontendId.replace(/[^0-9]/g, '')) || 1; +/** @deprecated Prefer numeric catalog ids from GET /menu/public */ +export const getBackendMenuId = (frontendId: string | number): number => { + if (typeof frontendId === 'number') return frontendId; + return parseInt(String(frontendId).replace(/[^0-9]/g, ''), 10) || 1; }; -// Map back-end ID to front-end menu item -export const getFrontendMenuItem = (backendId: number | string): MenuItem | undefined => { - const numId = typeof backendId === 'number' ? backendId : parseInt(String(backendId)) || 1; - return MENU_ITEMS.find(m => m.id === `m${numId}`) || MENU_ITEMS[0]; +export const getFrontendMenuItem = ( + backendId: number | string, + catalog: MenuItem[] = MENU_ITEMS, +): MenuItem | undefined => { + const numId = typeof backendId === 'number' ? backendId : parseInt(String(backendId), 10) || 1; + return catalog.find((m) => m.id === numId) || catalog[0]; }; // Connection State tracking @@ -107,6 +209,13 @@ const mockDb = { async function apiRequest(path: string, options: RequestInit = {}): Promise { const url = `${BASE_URL}${path}`; const headers = new Headers(options.headers || {}); + const method = (options.method || 'GET').toUpperCase(); + const isAiOrSession = + path.startsWith('/ai') || + path.includes('/start-session') || + path.startsWith('/tables/public') || + path.startsWith('/menu/public'); + const started = performance.now(); // Inject Staff Auth Token const staffToken = getAccessToken(); @@ -124,13 +233,27 @@ async function apiRequest(path: string, options: RequestInit = {}): Promise if (!headers.has('Content-Type') && !(options.body instanceof FormData)) { headers.set('Content-Type', 'application/json'); } + // Let the browser set multipart boundary for FormData + if (options.body instanceof FormData && headers.has('Content-Type')) { + headers.delete('Content-Type'); + } + + if (isAiOrSession) { + debugLog.info('api', `${method} ${path} →`, { + hasSession: Boolean(customerToken), + sessionToken: maskToken(customerToken), + hasStaff: Boolean(staffToken), + bodyPreview: + typeof options.body === 'string' ? options.body.slice(0, 180) : options.body ? '(FormData)' : undefined, + }); + } try { const res = await fetch(url, { ...options, headers }); triggerDemoMode(false); - if (res.status === 401 && staffToken) { - // Token expired, clear it + if (res.status === 401 && staffToken && !customerToken) { + // Only clear staff auth when this was a staff-only request. clearTokens(); window.location.reload(); throw new Error('Staff session expired. Please log in again.'); @@ -144,11 +267,49 @@ async function apiRequest(path: string, options: RequestInit = {}): Promise } catch { parsedErr = { error: errText }; } - throw new Error(parsedErr.error || parsedErr.message || `Request failed: ${res.status}`); + const detail = parsedErr.detail; + const detailText = + typeof detail === 'string' + ? detail + : Array.isArray(detail) + ? detail.map((d: { msg?: string }) => d.msg || JSON.stringify(d)).join('; ') + : detail + ? JSON.stringify(detail) + : ''; + const message = + detailText || + parsedErr.error || + parsedErr.message || + `Request failed: ${res.status}`; + if (isAiOrSession) { + debugLog.error('api', `${method} ${path} failed`, { + status: res.status, + ms: Math.round(performance.now() - started), + detail: String(message).slice(0, 300), + }); + } + throw new Error(message); } - return await res.json(); + const json = await res.json(); + if (isAiOrSession) { + debugLog.info('api', `${method} ${path} ok`, { + status: res.status, + ms: Math.round(performance.now() - started), + keys: json && typeof json === 'object' ? Object.keys(json) : typeof json, + }); + } + return json; } catch (error) { + if (isAiOrSession) { + debugLog.error('api', `${method} ${path} exception`, { + ms: Math.round(performance.now() - started), + error: error instanceof Error ? error.message : String(error), + }); + } + if (!DEMO_FALLBACK) { + throw error instanceof Error ? error : new Error(String(error)); + } console.warn(`API Request to ${path} failed, falling back to mock database.`, error); triggerDemoMode(true); return handleMockRequest(path, options); @@ -317,9 +478,47 @@ function handleMockRequest(path: string, options: RequestInit): any { return mockDb.bills.find(b => b.id === billId) || mockDb.bills[0]; } - // 7. KDS Board + // 7. KDS Board + staff test order if (path.startsWith('/kds/')) { - return mockDb.orders; + const parts = path.split('/').filter(Boolean); + // /kds/{id}/test-order + if (parts.length >= 3 && parts[2] === 'test-order' && method === 'POST') { + const newOrder = { + id: `ord-mock-${Date.now()}`, + order_id: Date.now(), + ticketNumber: String(100 + mockDb.orders.length + 1), + table_number: 'T1', + channel: 'manual_dine_in', + orderType: 'Dine-In', + status: 'placed', + placed_at: new Date().toISOString(), + createdAt: new Date().toISOString(), + timeElapsedMinutes: 0, + items: [ + { + id: `oi-${Date.now()}-1`, + menu_item_id: 9, + name: 'Butter Chicken', + quantity: 1, + unit_price: 380, + kds_status: 'queued', + }, + { + id: `oi-${Date.now()}-2`, + menu_item_id: 16, + name: 'Garlic Naan', + quantity: 2, + unit_price: 60, + kds_status: 'queued', + }, + ], + }; + mockDb.orders.push(newOrder); + notifyWsListeners({ type: 'kds.item_updated' }); + return newOrder; + } + // /kds/{id}/board + return { restaurant_id: Number(parts[1]) || 1, orders: mockDb.orders }; } // Patch KDS order item status @@ -555,11 +754,16 @@ const notifyWsListeners = (event: any) => { }); }; +export type WsConnectionState = 'idle' | 'connecting' | 'open' | 'reconnecting' | 'closed'; + export class RestroWebSocket { private ws: WebSocket | null = null; private url: string; - private reconnectTimer: any = null; + private reconnectTimer: ReturnType | null = null; private isClosedIntentional = false; + private hadOpenConnection = false; + onStateChange: ((state: WsConnectionState) => void) | null = null; + onReconnected: (() => void) | null = null; constructor(channel: 'kds' | 'orders', param: string) { if (channel === 'kds') { @@ -571,15 +775,30 @@ export class RestroWebSocket { } } + private setState(state: WsConnectionState) { + this.onStateChange?.(state); + } + connect() { if (isDemoMode) { console.log('Skipping real WebSocket connection in Demo Mode'); + this.setState('open'); return; } this.isClosedIntentional = false; + this.setState(this.hadOpenConnection ? 'reconnecting' : 'connecting'); try { this.ws = new WebSocket(this.url); - + + this.ws.onopen = () => { + const reconnected = this.hadOpenConnection; + this.hadOpenConnection = true; + this.setState('open'); + if (reconnected) { + this.onReconnected?.(); + } + }; + this.ws.onmessage = (event) => { try { const data = JSON.parse(event.data); @@ -592,7 +811,10 @@ export class RestroWebSocket { this.ws.onclose = () => { if (!this.isClosedIntentional) { console.log('WebSocket closed. Attempting reconnect in 5s...'); + this.setState('reconnecting'); this.reconnectTimer = setTimeout(() => this.connect(), 5000); + } else { + this.setState('closed'); } }; @@ -602,6 +824,8 @@ export class RestroWebSocket { }; } catch (e) { console.error('WebSocket connection setup failed', e); + this.setState('reconnecting'); + this.reconnectTimer = setTimeout(() => this.connect(), 5000); } } @@ -612,6 +836,7 @@ export class RestroWebSocket { this.ws.close(); this.ws = null; } + this.setState('closed'); } } @@ -646,19 +871,143 @@ export const api = { return !!getAccessToken(); }, + getMe: async (): Promise<{ + id: number; + email: string; + full_name: string; + restaurant_id: number; + home_restaurant_id?: number; + role: string; + is_active: boolean; + restaurants?: { id: number; name: string; is_home: boolean; is_active: boolean }[]; + }> => { + return apiRequest('/auth/me'); + }, + + switchRestaurant: async (restaurantId: number) => { + const res = await apiRequest('/auth/switch-restaurant', { + method: 'POST', + body: JSON.stringify({ restaurant_id: restaurantId }), + }); + if (res.access_token) { + setTokens(res.access_token, res.refresh_token); + } + return res; + }, + + aiChatTurn: async (payload: { + transcript: string; + language?: string; + submit?: boolean; + }): Promise<{ + transcript: string; + assistant_text: string; + cart: { + menu_item_id: number; + name: string; + quantity: number; + unit_price: number | string; + notes: string[]; + is_available: boolean; + }[]; + order: { order_id: number; subtotal: number | string; channel: string; item_count: number } | null; + tool_calls: string[]; + }> => { + const controller = new AbortController(); + const timer = window.setTimeout(() => controller.abort(), 25_000); + try { + return await apiRequest('/ai/chat-turn', { + method: 'POST', + body: JSON.stringify(payload), + signal: controller.signal, + }); + } catch (err) { + if (err instanceof DOMException && err.name === 'AbortError') { + throw new Error('AI is taking too long — please try again in a moment.'); + } + throw err; + } finally { + window.clearTimeout(timer); + } + }, + + /** Staff JWT: inventory / kitchen assistant. */ + staffAiChatTurn: async (payload: { + transcript: string; + language?: string; + }): Promise<{ + transcript: string; + assistant_text: string; + tool_calls: string[]; + low_stock: { + id: number; + name: string; + unit: string; + current_stock: number; + reorder_threshold: number; + is_low: boolean; + }[]; + updates?: { + id: number; + name: string; + unit: string; + current_stock: number; + reorder_threshold: number; + is_low: boolean; + }[]; + notify?: Record | null; + }> => { + const controller = new AbortController(); + const timer = window.setTimeout(() => controller.abort(), 25_000); + try { + return await apiRequest('/ai/staff-chat-turn', { + method: 'POST', + body: JSON.stringify(payload), + signal: controller.signal, + }); + } catch (err) { + if (err instanceof DOMException && err.name === 'AbortError') { + throw new Error('Staff AI is taking too long — please try again.'); + } + throw err; + } finally { + window.clearTimeout(timer); + } + }, + // Tables & Session - startSession: async (tableId: number, language = 'en') => { + startSession: async (tableId: number, language = 'en', tableNumber?: string) => { const res = await apiRequest(`/tables/${tableId}/start-session`, { method: 'POST', body: JSON.stringify({ language }) }); if (res.session_token) { - setCustomerSession(res.session_token, String(res.session_id), String(res.table_id), String(res.restaurant_id)); + setCustomerSession( + res.session_token, + String(res.session_id), + String(res.table_id), + String(res.restaurant_id), + tableNumber, + ); } return res; }, + /** Guest table picker (no auth). Defaults to first restaurant. */ + getPublicTables: async (opts?: { + restaurantId?: number; + availableOnly?: boolean; + }): Promise< + { id: number; restaurant_id: number; table_number: string; capacity: number; status: string }[] + > => { + const params = new URLSearchParams(); + if (opts?.restaurantId != null) params.set('restaurant_id', String(opts.restaurantId)); + if (opts?.availableOnly === false) params.set('available_only', 'false'); + const qs = params.toString() ? `?${params}` : ''; + return apiRequest(`/tables/public${qs}`); + }, + closeSession: async (tableId: number) => { return apiRequest(`/tables/${tableId}/close-session`, { method: 'POST' }); }, @@ -694,8 +1043,17 @@ export const api = { }); }, + /** Staff KDS: create a sample ticket without a guest session. */ + createKdsTestOrder: async (restaurantId: string | number) => { + return apiRequest(`/kds/${restaurantId}/test-order`, { method: 'POST' }); + }, + getOrderForSession: async (sessionId: string) => { - return apiRequest(`/orders/${sessionId}`); + const data = await apiRequest(`/orders/${sessionId}`); + if (Array.isArray(data)) { + return data.length ? data[data.length - 1] : null; + } + return data; }, getStaffOrderForSession: async (sessionId: string) => { @@ -718,10 +1076,39 @@ export const api = { }, // Billing - generateBill: async (orderId: string) => { + generateBill: async (orderId: string | number) => { return apiRequest(`/orders/${orderId}/generate-bill`, { method: 'POST' }); }, + /** Customer session: generate or return existing bill for an order. */ + sessionGenerateBill: async (orderId: string | number) => { + return apiRequest(`/session/orders/${orderId}/generate-bill`, { method: 'POST' }); + }, + + sessionGetBill: async (billId: string | number) => { + return apiRequest(`/session/bills/${billId}`); + }, + + sessionCheckoutRazorpay: async (billId: string | number) => { + return apiRequest(`/session/bills/${billId}/checkout/razorpay`, { method: 'POST' }); + }, + + sessionConfirmRazorpay: async ( + billId: string | number, + payload: { + razorpay_order_id: string; + razorpay_payment_id: string; + razorpay_signature: string; + amount?: number; + method?: string; + } + ) => { + return apiRequest(`/session/bills/${billId}/confirm-razorpay`, { + method: 'POST', + body: JSON.stringify(payload), + }); + }, + recordPayment: async (billId: string, method: string, amount: number) => { return apiRequest(`/bills/${billId}/record-payment`, { method: 'POST', @@ -766,6 +1153,10 @@ export const api = { return apiRequest('/inventory/low-stock'); }, + notifyLowStock: async () => { + return apiRequest('/inventory/notify-low-stock', { method: 'POST' }); + }, + // Recipes getRecipe: async (menuItemId: number) => { return apiRequest(`/menu-items/${menuItemId}/recipe`); @@ -779,6 +1170,65 @@ export const api = { }); }, + // Menu catalog + getPublicMenu: async (): Promise => { + const known = + typeof localStorage !== 'undefined' + ? localStorage.getItem('menu_catalog_updated_at') || '' + : ''; + const qs = known ? `?updated_at=${encodeURIComponent(known)}` : ''; + const data = (await apiRequest(`/menu/public${qs}`)) as PublicMenuResponse; + if (data?.updated_at && typeof localStorage !== 'undefined') { + localStorage.setItem('menu_catalog_updated_at', data.updated_at); + } + return data; + }, + + listMenuCategories: async (): Promise => { + return apiRequest('/menu/categories'); + }, + + createMenuCategory: async (payload: { name: string; display_order?: number }) => { + return apiRequest('/menu/categories', { + method: 'POST', + body: JSON.stringify(payload), + }); + }, + + updateMenuCategory: async (id: number, payload: { name?: string; display_order?: number }) => { + return apiRequest(`/menu/categories/${id}`, { + method: 'PATCH', + body: JSON.stringify(payload), + }); + }, + + deleteMenuCategory: async (id: number) => { + return apiRequest(`/menu/categories/${id}`, { method: 'DELETE' }); + }, + + listMenuItems: async (categoryId?: number): Promise => { + const qs = categoryId != null ? `?category_id=${categoryId}` : ''; + return apiRequest(`/menu/items${qs}`); + }, + + createMenuItem: async (payload: Record): Promise => { + return apiRequest('/menu/items', { + method: 'POST', + body: JSON.stringify(payload), + }); + }, + + updateMenuItem: async (id: number, payload: Record): Promise => { + return apiRequest(`/menu/items/${id}`, { + method: 'PATCH', + body: JSON.stringify(payload), + }); + }, + + deleteMenuItem: async (id: number) => { + return apiRequest(`/menu/items/${id}`, { method: 'DELETE' }); + }, + // Suppliers CRUD getSuppliers: async () => { return apiRequest('/suppliers'); diff --git a/src/theme/theme.ts b/src/theme/theme.ts index 5cf61a1..35142c8 100644 --- a/src/theme/theme.ts +++ b/src/theme/theme.ts @@ -1,145 +1,2 @@ -import { createTheme } from '@mui/material/styles'; - -export const theme = createTheme({ - palette: { - mode: 'light', - primary: { - main: '#ac2d00', - light: '#ffb5a0', - dark: '#872100', - contrastText: '#ffffff', - }, - secondary: { - main: '#546067', - light: '#818e95', - dark: '#2a363d', - contrastText: '#ffffff', - }, - background: { - default: '#f8f9fa', - paper: '#ffffff', - }, - error: { - main: '#ba1a1a', - light: '#ffdad6', - dark: '#93000a', - }, - warning: { - main: '#845000', - light: '#ffddba', - dark: '#2b1700', - }, - success: { - main: '#11651d', - light: '#a3f69c', - dark: '#003915', - }, - info: { - main: '#00a6e0', - light: '#c4e7ff', - dark: '#00374d', - }, - text: { - primary: '#1a1c1c', - secondary: '#5b4139', - }, - divider: '#e4beb4', - }, - typography: { - fontFamily: '"Inter", "Plus Jakarta Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', - h1: { - fontFamily: '"Plus Jakarta Sans", "Inter", sans-serif', - fontWeight: 800, - letterSpacing: '-0.02em', - }, - h2: { - fontFamily: '"Plus Jakarta Sans", "Inter", sans-serif', - fontWeight: 700, - letterSpacing: '-0.01em', - }, - h3: { - fontFamily: '"Plus Jakarta Sans", "Inter", sans-serif', - fontWeight: 700, - }, - h4: { - fontFamily: '"Plus Jakarta Sans", "Inter", sans-serif', - fontWeight: 600, - }, - h5: { - fontFamily: '"Inter", sans-serif', - fontWeight: 600, - }, - h6: { - fontFamily: '"Inter", sans-serif', - fontWeight: 600, - }, - subtitle1: { - fontFamily: '"Inter", sans-serif', - fontWeight: 600, - }, - body1: { - fontFamily: '"Inter", sans-serif', - lineHeight: 1.5, - }, - body2: { - fontFamily: '"Inter", sans-serif', - lineHeight: 1.43, - }, - button: { - fontFamily: '"Inter", sans-serif', - fontWeight: 600, - textTransform: 'none', - }, - caption: { - fontFamily: '"JetBrains Mono", monospace', - fontWeight: 500, - }, - }, - shape: { - borderRadius: 8, - }, - components: { - MuiButton: { - styleOverrides: { - root: { - borderRadius: 8, - padding: '8px 16px', - boxShadow: 'none', - '&:hover': { - boxShadow: '0px 2px 8px rgba(172, 45, 0, 0.25)', - }, - }, - contained: { - background: 'linear-gradient(135deg, #ac2d00 0%, #d53e0b 100%)', - }, - }, - }, - MuiCard: { - styleOverrides: { - root: { - borderRadius: 12, - boxShadow: '0px 2px 12px rgba(0, 0, 0, 0.05)', - border: '1px solid rgba(228, 190, 180, 0.4)', - }, - }, - }, - MuiChip: { - styleOverrides: { - root: { - fontWeight: 600, - borderRadius: 6, - }, - }, - }, - MuiAppBar: { - styleOverrides: { - root: { - backgroundColor: '#ffffff', - color: '#1a1c1c', - boxShadow: '0px 1px 10px rgba(0,0,0,0.05)', - borderBottom: '1px solid #e2e2e2', - }, - }, - }, - }, -}); +/** Re-export shared theme from @restroai/ui. */ +export { theme } from '@restroai/ui'; diff --git a/src/utils/debugLog.ts b/src/utils/debugLog.ts new file mode 100644 index 0000000..609d2ba --- /dev/null +++ b/src/utils/debugLog.ts @@ -0,0 +1,46 @@ +/** + * Browser debug logger for RestroAI guest/AI flows. + * Enable with localStorage.setItem('restroai_debug', '1') or ?debug=1 + * Always logs warn/error; info/debug only when enabled. + */ +const PREFIX = '[RestroAI]'; + +function debugEnabled(): boolean { + if (typeof window === 'undefined') return false; + try { + if (localStorage.getItem('restroai_debug') === '1') return true; + if (new URLSearchParams(window.location.search).get('debug') === '1') return true; + } catch { + /* ignore */ + } + // Default ON in Vite dev so Chrome DevTools always shows AI/session traces. + return Boolean(import.meta.env.DEV); +} + +type LogPayload = Record | undefined; + +function fmt(scope: string, message: string, payload?: LogPayload): unknown[] { + const ts = new Date().toISOString().slice(11, 23); + if (payload === undefined) return [`${PREFIX} ${ts} ${scope} ${message}`]; + return [`${PREFIX} ${ts} ${scope} ${message}`, payload]; +} + +export const debugLog = { + enabled: debugEnabled, + info(scope: string, message: string, payload?: LogPayload) { + if (!debugEnabled()) return; + console.info(...fmt(scope, message, payload)); + }, + warn(scope: string, message: string, payload?: LogPayload) { + console.warn(...fmt(scope, message, payload)); + }, + error(scope: string, message: string, payload?: LogPayload) { + console.error(...fmt(scope, message, payload)); + }, +}; + +export function maskToken(token: string | null | undefined): string { + if (!token) return '(none)'; + if (token.length <= 12) return `${token.slice(0, 4)}…`; + return `${token.slice(0, 6)}…${token.slice(-4)} (len=${token.length})`; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..64251fb --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/staff.html b/staff.html new file mode 100644 index 0000000..19bfbdb --- /dev/null +++ b/staff.html @@ -0,0 +1,18 @@ + + + + + + + + + RestroAI Staff Dashboard + + + + + +
+ + + diff --git a/tsconfig.app.json b/tsconfig.app.json index 6830b6f..b5ff342 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -4,11 +4,16 @@ "target": "es2023", "lib": ["ES2023", "DOM"], "module": "esnext", - "types": ["vite/client"], + "types": ["vite/client", "vite-plugin-pwa/client"], "allowArbitraryExtensions": true, "skipLibCheck": true, + "ignoreDeprecations": "6.0", + "baseUrl": ".", + "paths": { + "@restroai/ui": ["packages/ui/src/index.ts"], + "@restroai/ui/*": ["packages/ui/src/*"] + }, - /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, @@ -16,11 +21,10 @@ "noEmit": true, "jsx": "react-jsx", - /* Linting */ "noUnusedLocals": true, "noUnusedParameters": true, "erasableSyntaxOnly": true, "noFallthroughCasesInSwitch": true }, - "include": ["src"] + "include": ["src", "packages/ui/src"] } diff --git a/vite.config.ts b/vite.config.ts index 8b0f57b..50cec05 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,7 +1,86 @@ -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { VitePWA } from 'vite-plugin-pwa'; +import path from 'node:path'; // https://vite.dev/config/ export default defineConfig({ - plugins: [react()], -}) + plugins: [ + react(), + VitePWA({ + registerType: 'autoUpdate', + includeAssets: ['favicon.svg', 'icons.svg'], + manifest: { + name: 'RestroAI Guest', + short_name: 'RestroAI', + description: 'Scan, order, and pay at your table', + theme_color: '#ac2d00', + background_color: '#fff9f7', + display: 'standalone', + start_url: '/', + scope: '/', + icons: [ + { + src: '/icons.svg', + sizes: 'any', + type: 'image/svg+xml', + purpose: 'any maskable', + }, + ], + }, + workbox: { + globPatterns: ['**/*.{js,css,html,ico,png,svg,webp,woff2}'], + navigateFallback: '/index.html', + navigateFallbackDenylist: [/^\/staff\.html/, /^\/api/], + runtimeCaching: [ + { + urlPattern: ({ url }) => url.pathname.includes('/menu/public'), + handler: 'StaleWhileRevalidate', + options: { + cacheName: 'restroai-menu', + expiration: { + maxEntries: 20, + maxAgeSeconds: 60 * 60 * 24, + }, + cacheableResponse: { + statuses: [0, 200], + }, + }, + }, + { + urlPattern: ({ request }) => request.destination === 'image', + handler: 'CacheFirst', + options: { + cacheName: 'restroai-images', + expiration: { + maxEntries: 60, + maxAgeSeconds: 60 * 60 * 24 * 7, + }, + }, + }, + ], + }, + // Dev uses Vite HMR; SW/precache only in production builds (avoids empty dev-dist warning). + devOptions: { + enabled: false, + }, + }), + ], + resolve: { + alias: { + '@restroai/ui': path.resolve(__dirname, 'packages/ui/src'), + }, + }, + build: { + rollupOptions: { + input: { + main: path.resolve(__dirname, 'index.html'), + staff: path.resolve(__dirname, 'staff.html'), + }, + }, + }, + server: { + host: '0.0.0.0', + port: 5173, + }, +});