diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 998f399..0000000 --- a/.eslintrc +++ /dev/null @@ -1,131 +0,0 @@ -{ - "extends": "eslint:recommended", - "env": { - "node": true, - "mocha": true - }, - "rules": { - "array-bracket-spacing": [ - 2, - "never" - ], - "brace-style": [ - 2, - "1tbs" - ], - "consistent-return": 0, - "indent": [ - 2, - 2 - ], - "no-multiple-empty-lines": [ - 2, - { - "max": 2 - } - ], - "no-use-before-define": [ - 2, - "nofunc" - ], - "one-var": [ - 2, - "never" - ], - "quote-props": [ - 2, - "as-needed" - ], - "quotes": [ - 2, - "single" - ], - "space-after-keywords": [ - 2, - "always" - ], - "space-before-function-paren": [ - 2, - { - "anonymous": "always", - "named": "never" - } - ], - "space-in-parens": [ - 2, - "never" - ], - "strict": [ - 2, - "global" - ], - "curly": [ - 2, - "all" - ], - "eol-last": 2, - "key-spacing": [ - 2, - { - "beforeColon": false, - "afterColon": true - } - ], - "no-eval": 2, - "no-with": 2, - "space-infix-ops": 2, - "dot-notation": [ - 2, - { - "allowKeywords": true - } - ], - "eqeqeq": 2, - "no-alert": 2, - "no-caller": 2, - "no-empty-label": 2, - "no-extend-native": 2, - "no-extra-bind": 2, - "no-implied-eval": 2, - "no-iterator": 2, - "no-label-var": 2, - "no-labels": 2, - "no-lone-blocks": 2, - "no-loop-func": 2, - "no-multi-spaces": 2, - "no-multi-str": 2, - "no-native-reassign": 2, - "no-new": 2, - "no-new-func": 2, - "no-new-wrappers": 2, - "no-octal-escape": 2, - "no-proto": 2, - "no-return-assign": 2, - "no-script-url": 2, - "no-sequences": 2, - "no-unused-expressions": 2, - "yoda": 2, - "no-shadow": 2, - "no-shadow-restricted-names": 2, - "no-undef-init": 2, - "camelcase": 2, - "comma-spacing": 2, - "new-cap": 2, - "new-parens": 2, - "no-array-constructor": 2, - "no-extra-parens": 2, - "no-new-object": 2, - "no-spaced-func": 2, - "no-trailing-spaces": 2, - "no-underscore-dangle": 2, - "semi": 2, - "semi-spacing": [ - 2, - { - "before": false, - "after": true - } - ], - "space-return-throw-case": 2 - } -} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..92b19d6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,21 @@ +name: CI + +on: + push: + branches: [master] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm typecheck + - run: pnpm test + - run: pnpm build diff --git a/.gitignore b/.gitignore index 98f9955..e2c9dd5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ node_modules +dist coverage .idea +.DS_Store +*.tsbuildinfo diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index fa804ef..0000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: node_js -node_js: - - v5 - - v4 - - '0.12' - - '0.10' diff --git a/README.md b/README.md index 04f0534..3fddc68 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,143 @@ -# gtv-bfy [![NPM version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Dependency Status][daviddm-image]][daviddm-url] -> Jquery google tv adapcion, libreria para desarrollo de aplicaciones para smart-TV +# gtvzone -## Installation +Navegación espacial con control remoto para apps de Smart TV (Tizen, webOS, VIDAA, HbbTV). +Un **core sin DOM ni framework** decide a dónde va el foco; **bindings finos** lo conectan con +el navegador, React o Angular. -```sh -$ npm install --save @arxis/gtvzone -``` +| Paquete | Para qué | +|---|---| +| [`@arxis/gtvzone-core`](packages/core) | Motor: registro, zonas, estrategias, capas y store con `subscribe`. Cero dependencias. | +| [`@arxis/gtvzone-dom`](packages/dom) | Adapter de navegador (medición, orden del DOM), teclas por plataforma y **modo selectores** para apps sin framework. | +| [`@arxis/gtvzone-react`](packages/react) | `useFocusable`, ``, ``. React ≥ 16.8. | +| [`@arxis/gtvzone-angular`](packages/angular) | Directivas `gtvFocusable` / `gtvZone` / `gtvLayer` y servicio con signals. Angular ≥ 16.1. | + +## Conceptos + +- **Elemento enfocable:** algo que puede recibir el foco. Se identifica con un `id` estable elegido por vos. +- **Zona:** agrupa elementos y otras zonas. Cada zona tiene una **estrategia**: + - `geometry` (default): el vecino más cercano en la dirección, midiendo rectángulos. + - `list`: fila o columna por posición. **No mide el layout** al moverse dentro de la zona. + - `grid`: filas y columnas con `columns`. Tampoco mide al moverse dentro. + - Una estrategia propia: cualquier objeto con `rank()` y `enter()`. +- Al salir de una zona por un borde, la búsqueda sube a la zona padre. Al entrar a una zona se + prioriza el último elemento enfocado (`saveLastFocused`), después `firstFocus`, después el más + cercano al origen. +- **Capa:** aísla un conjunto de zonas (un modal, un menú). Solo la capa activa, la de mayor + prioridad, es navegable. Al cerrarla, el foco vuelve a donde estaba. +- **Overrides:** `nextFocus` por elemento y `nextZone` por zona fuerzan un destino o bloquean + una dirección (`null`). `trap` impide salir de una zona con las flechas. + +El estado (`focusedId`, `activeLayer`) vive **solo en el core**. Los bindings se suscriben y +pintan; nunca guardan una copia propia del foco. -## Usage +## Uso + +### Sin framework (modo selectores) ```js -var gtvBfy = require('@arxis/gtvzone'); +import { bindZone, createDomNavigation } from '@arxis/gtvzone-dom' + +const nav = createDomNavigation() // adapter DOM + teclas de la plataforma detectada + +bindZone(nav, '#menu', { item: '.item', strategy: 'list', orientation: 'vertical' }) +bindZone(nav, '#content', { item: '.card' }) +bindZone(nav, '#row-1', { item: '.card', strategy: 'list' }) // se anida en #content solo + +document.addEventListener('gtvzone:press', e => play(e.target)) +``` + +El ítem enfocado recibe la clase `gtv-focused`. Un `MutationObserver` registra y da de baja +los ítems cuando cambia el DOM. Enlazá primero la zona contenedora y después las internas. +Hay un ejemplo completo en [`examples/vanilla`](examples/vanilla/index.html). + +### React + +```jsx +import { createDomNavigation } from '@arxis/gtvzone-dom' +import { FocusLayer, FocusZone, NavigationProvider, useFocusable } from '@arxis/gtvzone-react' + +const nav = createDomNavigation() + +function Card ({ item }) { + const { ref, focused } = useFocusable({ id: item.id, onPress: () => play(item) }) + return
{item.title}
+} + + + + {items.map(item => )} + + {open && ( + + … + + )} + +``` -gtvBfy('Rainbow'); +Los hooks usan `useSyncExternalStore` (con shim para React 17): cada elemento se vuelve a +renderizar solo cuando cambia **su** estado de foco. `FocusZone` reenvía `ref`. + +### Angular + +```ts +bootstrapApplication(AppComponent, { providers: [provideGtvNavigation()] }) +``` + +```html +
+ @for (item of items; track item.id) { +
{{ item.title }}
+ } +
+ +
…
+``` + +El listener de teclas corre **fuera de la zona de Angular**. Solo se entra a la zona (y corre +la detección de cambios) cuando el foco cambia de verdad. El estado está disponible como signals +(`focusedId`, `activeLayer`, `isFocused(id)`) y como `state$`. + +## Compatibilidad con TVs + +- `core`, `dom` y `react` se compilan con target `chrome63`, el de Tizen 5.0. webOS 5 trae + Chromium 68. Sin optional chaining ni `??` en la salida. +- `angular` se publica en formato APF (ES2022). La CLI de Angular de la app lo baja según su + `browserslist`. +- El adapter DOM acepta `getRect` para enchufar un polyfill de medición en TVs donde + `getBoundingClientRect` falla dentro de contenedores transformados. +- Mapas de teclas incluidos: `tizen` (Back 10009), `webos` (461), `hisense` (8) y `browser` + (Esc/Backspace). Se detectan por user agent y se pueden reemplazar con `keyMap`. + +## Migración desde 0.x + +La 0.x era el *Google TV jQuery UI Library* (2010) con jQuery 2. La 1.0 es una reescritura: + +| 0.x (`gtv.jq`) | 1.0 | +|---|---| +| `KeyBehaviorZone` + `containerSelector` / `navSelectors` | `bindZone(nav, container, { item })` o `registerZone` | +| `useGeometry: true` | `strategy: 'geometry'` (default) | +| filas con `itemRow` | `strategy: 'list'` o `'grid'` | +| `saveRowPosition` | `saveLastFocused` (default `true`) | +| `KeyZoneLayer`, `createLayer` / `setLayer` | `openLayer(id, { priority })` / `closeLayer(id)` | +| `selectionClasses.basic` | `focusedClass` (default `gtv-focused`) | +| `actions.click` | `onPress` / evento `gtvzone:press` | +| `setGlobalKeyMapping` | `bindKeys(nav, { keyMap })` | + +Las versiones 0.x siguen disponibles en npm como `@arxis/gtvzone`. + +## Desarrollo + +```sh +pnpm install +pnpm test # vitest: core (node), dom/react/angular (jsdom) +pnpm typecheck +pnpm build # tsup para core/dom/react, ng-packagr para angular ``` -## License -MIT © [renearias]() +Los tests corren contra los fuentes (`src`) de los paquetes hermanos, sin build previo. +## Licencia -[npm-image]: https://badge.fury.io/js/gtv-bfy.svg -[npm-url]: https://npmjs.org/package/@arxis/gtvzone -[travis-image]: https://travis-ci.org/rikhart/gtv-bfy.svg?branch=master -[travis-url]: https://travis-ci.org/rikhart/gtv-bfy -[daviddm-image]: https://david-dm.org/rikhart/gtv-bfy.svg?theme=shields.io -[daviddm-url]: https://david-dm.org/rikhart/gtv-bfy +MIT © renearias diff --git a/examples/vanilla/index.html b/examples/vanilla/index.html new file mode 100644 index 0000000..1aeb99b --- /dev/null +++ b/examples/vanilla/index.html @@ -0,0 +1,85 @@ + + + + + gtvzone · ejemplo vanilla + + + + + + +
+

Continuar viendo

+
+

Recomendados

+
+
+ +
Flechas para navegar · Enter abre el detalle · Esc cierra
+ + + + diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index 77aaf60..0000000 --- a/gulpfile.js +++ /dev/null @@ -1,47 +0,0 @@ -'use strict'; -var path = require('path'); -var gulp = require('gulp'); -var eslint = require('gulp-eslint'); -var excludeGitignore = require('gulp-exclude-gitignore'); -var mocha = require('gulp-mocha'); -var istanbul = require('gulp-istanbul'); -var nsp = require('gulp-nsp'); -var plumber = require('gulp-plumber'); - -gulp.task('static', function () { - return gulp.src('**/*.js') - .pipe(excludeGitignore()) - .pipe(eslint()) - .pipe(eslint.format()) - .pipe(eslint.failAfterError()); -}); - -gulp.task('nsp', function (cb) { - nsp({package: path.resolve('package.json')}, cb); -}); - -gulp.task('pre-test', function () { - return gulp.src('lib/**/*.js') - .pipe(istanbul({ - includeUntested: true - })) - .pipe(istanbul.hookRequire()); -}); - -gulp.task('test', ['pre-test'], function (cb) { - var mochaErr; - - gulp.src('test/**/*.js') - .pipe(plumber()) - .pipe(mocha({reporter: 'spec'})) - .on('error', function (err) { - mochaErr = err; - }) - .pipe(istanbul.writeReports()) - .on('end', function () { - cb(mochaErr); - }); -}); - -gulp.task('prepublish', ['nsp']); -gulp.task('default', ['static', 'test']); diff --git a/lib/gtv/gtvcore.js b/lib/gtv/gtvcore.js deleted file mode 100644 index 8aba20a..0000000 --- a/lib/gtv/gtvcore.js +++ /dev/null @@ -1,558 +0,0 @@ -// Copyright 2010 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. - -/** - * @fileoverview Common classes used throughout the library. GtvCore contains - * public static methods for processing feeds. SynchronizedCallback - * can be used to aggregate multiple asynch callback events into a single - * call. - * - */ - - -module.exports = function ($) { - - var gtv = { - jq: {} - }; - - - /** - * Static class for utilities used by controls - * @constructor - * @private - */ - gtv.jq.GtvCore = function () { - }; - - /** - * Triggers a 'load' event on a container when images have finished loading - * @param {jQuery.Element} container Container to trigger after images load. - * If images not supplied, the container will be triggered when images - * in it have finished loading. - * @param {?Array.} images Optional image elements. - * Supply this if container is to be triggered on images that aren't - * descendants of it. - * @private - */ - gtv.jq.GtvCore.triggerOnLoad = function (container, images) { - images = images || container.find('img'); - var imgCount = images.length; - - if (imgCount == 0) { - container.trigger('load'); - return; - } - - images.each(function (index) { - if ($(this).complete) { - imgCount--; - if (imgCount == 0) { - container.trigger('load'); - } - } else { - images.load(function () { - imgCount--; - if (imgCount == 0) { - container.trigger('load'); - } - }); - } - }); - }; - - - /** - * Creates a function to retrieve the next item from an item parameter, - * which may be a generator function or an array. - * @param {ControlContents} controlContents A ControlContents object with either - * an items attribute or an itemsGenerator attribute set. - * @return {function(jQuery.Element)} A function adds the next item in a - * collection to the supplied container. - * @private - */ - gtv.jq.GtvCore.makeAddNextItemParams = function (controlContents) { - var addNextItem; - if (controlContents.itemsGenerator) { - // If a generator is specified, the return function is a pass-through that - // calls the generator. (This generator creates an item, adds it to the - // parent and returns true; or returns false if it has no item to add.) - addNextItem = function (parent) { - return controlContents.itemsGenerator(parent); - }; - } else if (controlContents.items) { - // If an items array is specified, return a function that adds the next - // item in the array to the parent and returns true; or returns false if - // the end of the array has been reached. - var index = 0; - addNextItem = function (parent) { - if (index >= controlContents.items.length) { - return false; - } - - var item = controlContents.items[index]; - if (!item) { - return false; - } - - index++; - parent.append(item); - return true; - }; - } - return addNextItem; - }; - - /** - * Reads a feed in ATOM format and makes callbacks to build an array of - * items from the feed. - * @param {string} feed URL to the feed to read. - * @param {function(Object)} makeItem A callback function that, when passed - * an entry from the feed returns a constructed item from it. - * @param {function(Array.)} doneCallback A callback that will be passed - * the array of all constructed items. - * @private - */ - gtv.jq.GtvCore.processAtomFeed = function (feed, makeItem, doneCallback) { - $.ajax({ - url: feed, - success: function (data) { - var itemsArray = []; - - var entries = $(data).find('entry'); - for (var i = 0; i < entries.length; i++) { - var item = makeItem(entries[i]); - if (item) { - itemsArray.push(item); - } - } - doneCallback(itemsArray); - } - }); - }; - - /** - * Reads a feed in JSONP format and makes callbacks to build an array of - * items from the feed. - * @param {string} feed URL to the feed to read. - * @param {function(Object)} makeItem A callback function that, when passed - * an entry from the feed returns a constructed item from it. - * @param {function(Array.)} doneCallback A callback that will be passed - * the array of all constructed items. - * @param {Array.)} entryKey An array of strings that represent, in - * hierarchical order the path to the array of entries in the returned feed. - * @private - */ - gtv.jq.GtvCore.processJsonpFeed = function (feed, - makeItem, - doneCallback, - entryKey) { - entryKey = entryKey || ['feed', 'entry']; - - $.ajax({ - url: feed, - dataType: 'jsonp', - success: function (data) { - var itemsArray = []; - - var entries = data; - for (var j = 0; j < entryKey.length; j++) - entries = entries[entryKey[j]]; - - for (var i = 0; i < entries.length; i++) { - var item = makeItem(entries[i]); - if (item) { - itemsArray.push(item); - } - } - - doneCallback(itemsArray); - } - }); - }; - - gtv.jq.GtvCore.doAjaxCall = function (url, dataType, jsonpCallback, callbackSuccess, callbackError) { - var options = { - url: url, - dataType: dataType, - error: function (httpRequest, textStatus, errorThrown) { - callbackError(errorThrown); - }, - success: function (data, textStatus, httpRequest) { - callbackSuccess(data); - } - }; - if (dataType == 'jsonp') { - options.jsonpCallback = jsonpCallback; - } - return $.ajax(options); - }; - - gtv.jq.GtvCore.getZoom = function () { - var zoom = parseFloat($(document.body).css('zoom')); - if (isNaN(zoom)) { - zoom = 1; - } - return zoom; - }; - - gtv.jq.GtvCore.getInt = function (value) { - value = parseInt(value); - if (isNaN(value)) { - value = 0; - } - - return value; - }; - - gtv.jq.GtvCore.preloadImages = function (images) { - var img = new Image(); - for (var i = 0; i < images.length; i++) { - img.src = images[i]; - } - }; - - gtv.jq.GtvCore.formatTime = function (seconds, unit) { - function padCero(num) { - if (num < 10) { - return '0' + num; - } - return num.toString(); - } - - switch (unit) { - case 'hours': - { - var hours = Math.floor(seconds / 3600); - var minutes = Math.floor(seconds % 3600); - return padCero(hours) + ':' + gtv.jq.GtvCore.formatTime(minutes, 'minutes'); - } - case 'minutes': - { - var minutes = Math.floor(seconds / 60); - seconds = Math.floor(seconds % 60); - return padCero(minutes) + ':' + gtv.jq.GtvCore.formatTime(seconds, 'seconds'); - } - case 'seconds': - { - return padCero(seconds); - } - } - return padCero(seconds); - }; - - - /** - * A class that tracks by reference count a number of requests for a single - * callback and makes sure that it is called once when all requests are - * completed. The constructor starts the count at 1 and expects this to be - * cleared by the creator calling done(). - * @constructor - * @param {function} callback The callback to make when all dependent requests - * are completed. - * @private - */ - gtv.jq.SynchronizedCallback = function (callback) { - this.expectedCallbacks = 1; - this.callback = callback; - }; - - /** - * Called to acquire a reference to the callback. - * @return {function} The callback function to make when a depedent request is - * completed. - */ - gtv.jq.SynchronizedCallback.prototype.acquireCallback = function () { - var synchronizedCallback = this; - - synchronizedCallback.expectedCallbacks++; - return function () { - synchronizedCallback.callbackFinished(); - }; - }; - - /** - * Creates a wrapper callback for acquiring a callback - * @return {function} A function that can be called to acquire the callback - * without access to the object instance. - */ - gtv.jq.SynchronizedCallback.prototype.getCallback = function () { - var synchronizedCallback = this; - - return function () { - return synchronizedCallback.acquireCallback(); - }; - }; - - /** - * Decrements the callback reference count and calls the primary callback - * if all dependent callbacks are completed. - * @private - */ - gtv.jq.SynchronizedCallback.prototype.callbackFinished = function () { - this.expectedCallbacks--; - if (this.expectedCallbacks == 0 && this.callback) - this.callback(); - }; - - /** - * Called by the code that originally constructed the object instance to - * represent that it is finished initiating tasks with dependent callbacks. - */ - gtv.jq.SynchronizedCallback.prototype.done = function () { - this.callbackFinished(); - }; - - - /** - * Holds parameters used to create the library controls. - * @param {?gtv.jq.CreationParams} opt_params CreationParams to initialize - * this new object with. - * @constructor - */ - gtv.jq.CreationParams = function (opt_params) { - var params = opt_params || {}; - - params.styles = params.styles || {}; - this.styles = params.styles; - this.styles.page = params.styles.page || ''; - this.styles.row = params.styles.row || ''; - this.styles.itemDiv = params.styles.itemDiv || ''; - this.styles.item = params.styles.item || ''; - this.styles.selected = params.styles.selected || 'item-hover'; - this.styles.hasData = params.styles.hasData || 'item-hover-active'; - this.styles.normal = params.styles.normal || ''; - this.styles.chosen = params.styles.chosen || ''; - - this.containerId = params.containerId; - if (!this.containerId) { - throw new Error('containerId must be provided'); - } - - this.choiceCallback = params.choiceCallback || - function () { - }; - - this.layerNames = params.layerNames; - - this.keyController = params.keyController; - }; - - /** - * Instance of the key controller this control is using. - * @type {KeyController} - */ - gtv.jq.CreationParams.prototype.keyController = null; - - /** - * CSS classes used to style the row. - * page {string} CSS class to style the container for each page of rows. - * row {string} CSS class to style the row. - * itemsDiv {string} CSS class to style the DIV holding the items. - * itemDiv {string} CSS class to style the DIV holding a single item. - * item {string} CSS class to style the individual item. - * selected {string} CSS class to style the item that has the selection. - * hasData {string} CSS class to style the item that has the selection, - * for controls that support different selection styles based on - * associated data (see gtv.jq.CaptionItem). - * normal {string} For controls that maintain a sticky item choice, such - * as SideNav. CSS class to style an item that has been 'chosen', that - * is, the ENTER key was pressed while it had selection, or it - * received a mouse click. - * chosen {string} For controls that maintain a sticky item choice, such - * as SideNav. CSS class to style an item that has been 'chosen', that - * is, the ENTER key was pressed while it had selection, or it - * received a mouse click. - * @type Object - */ - gtv.jq.CreationParams.prototype.styles = null; - - /** - * The ID of the control container (an element will be created with this ID). - * @type string - */ - gtv.jq.CreationParams.prototype.containerId = null; - - /** - * Callback to make when the user chooses an item (if applicable to the control) - * @type Function(selectedItem) - */ - gtv.jq.CreationParams.prototype.choiceCallback = null; - - /** - * Array of Layer name to add the control to, or 'default' if not supplied. - * @type string - */ - gtv.jq.CreationParams.prototype.layerNames = null; - - - /** - * Describes an item that has a caption and a data item to go with it. - * Not all controls support CaptionItem (StackControl, SlidingControl). - * Use this instead of a simple items array when an item that needs - * selection (say, a thumbnail) also needs a caption (say, photo title) - * but the selection outline should only be drawn around the item. - * @constructor - */ - gtv.jq.CaptionItem = function () { - }; - - /** - * The item that will be outlined by selection. This will be the child of - * the element with the CSS class styles.item. - * @type jQuery.Element - */ - gtv.jq.CaptionItem.prototype.item = null; - - /** - * The caption for the item, to be displayed beneath it. The container of this - * caption will be styled so that it will be clipped at the width of the item. - * @type jQuery.Element - */ - gtv.jq.CaptionItem.prototype.caption = null; - - /** - * If supplied, this item will be given an active hover style by the key - * controller when selected. This allows items with data to be visually - * distinguished from those without. For example, an item that can be - * navigated to when chosen might have this value non-null; and item that - * cannot will have it null. The two different selection styles applied - * (styles.selected, styles.hasData) provide a visual clue to the user. - */ - gtv.jq.CaptionItem.prototype.data = null; - - - /** - * Describes the contents to be added to a control. Passed with ShowParams - * object, used when calling showControl on a control object. - * - * items, itemsArray and itemsGenerator, indicate how new items will be - * added to the control being displayed. Only one of these should be supplied - * in an instance of the object. - * @param {gtv.jq.ControlContents} opt_params Optional initialization values. - * @constructor - */ - gtv.jq.ControlContents = function (opt_params) { - var params = opt_params || {}; - - this.items = params.items; - this.captionItems = params.captionItems; - this.contentsArray = params.contentsArray; - this.itemsGenerator = params.itemsGenerator; - }; - - /** - * Simple array of items to add to the control. These items will be added to - * the control in the order they appear in the array. Each item is added as - * a child of a separate Element to contain it. These container elements are - * created by the control and given the Styles.item CSS class. - * @type Array. - */ - gtv.jq.ControlContents.prototype.items = null; - - /** - * Simple array of items to add to the control. These items will be added to - * the control in the order they appear in the array. Each item is added as - * a child of a separate Element to contain it. These container elements are - * created by the control and given the Styles.item CSS class. - * @type Array. - */ - gtv.jq.ControlContents.prototype.captionItems = null; - - /** - * Array of ControlContents objects to add to a control. Some multi-row - * controls (such as the RollerControl) require this so that each row can be - * described explicitly. That is, each row has a very specific contents - * instead of being created as-needed and filled in. - * - * Since this is an array of ControlContents objects, each element is can - * in turn be a simple array of items or an itemsGenerator. (It could also - * take an itemsArray, but no existing controls take an array of arrays of - * arrays.) - * @type Array. - */ - gtv.jq.ControlContents.prototype.contentsArray = null; - - /** - * A function that generates items as-requested. Clients creating controls - * can use this to create one or more controls in a callback instead of having - * them pre-created an placed in an items array. Generally this callback - * maintains state in its closure and creates the each subsequent item in a - * collection when it is called. This function also must add its control to - * the parent element, which is passed in. The parent element is already in - * the page DOM when the callback is called, so controls added to the parent - * will be in the DOM immediately as well. - * - * This function should return boolean true if it added an element to the - * parent, and false if it did not (and has no further elements to add). - * @type Function(jQuery.Element) - */ - gtv.jq.ControlContents.itemsGenerator = null; - - /** - * Holds parameters used to show the library controls. - * @param {?gtv.jq.ShowParams} opt_params Optional parameters to initialize the - * new object with. - * @constructor - */ - gtv.jq.ShowParams = function (opt_params) { - var params = opt_params || {}; - - this.topParent = params.topParent; - if (!this.topParent) { - throw new Error("topParent must be supplied."); - } - - this.contents = new gtv.jq.ControlContents(params.contents); - }; - - /** - * Parent element on the page that holds the control. - * @type jQuery.Element - */ - gtv.jq.ShowParams.prototype.topParent = null; - - /** - * The contents the control should have. See gtv.jq.ControlContents for - * details. - * @type gtv.jq.ControlContents - */ - gtv.jq.ShowParams.prototype.contents = null; - - - /** - * Size class holds a width/height dimension pair. - * @param {number} width - * @param {number} height - * @constructor - */ - gtv.jq.Size = function (width, height) { - this.width = width; - this.height = height; - }; - - /** - * Width dimension in pixels. - * @type number - */ - gtv.jq.Size.prototype.width; - - /** - * Height dimension in pixels. - * @type number - */ - gtv.jq.Size.prototype.height; - - return gtv; -}; diff --git a/lib/gtv/keycontrol.js b/lib/gtv/keycontrol.js deleted file mode 100644 index 6f75472..0000000 --- a/lib/gtv/keycontrol.js +++ /dev/null @@ -1,1721 +0,0 @@ -// Copyright 2010 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. - -/** - * @fileoverview Classes for KeyBehaviorZone, KeyZoneLayer, KeyController. - * A page uses a KeyController to manage the selection of elements and make - * sure that only one element is selected at a time. The controller can - * manage selection between multiple logical areas of the page, or "zones". - * - * Each KeyBehaviorZone can have its own distinct behaviors for keys - * (including arrow keys), track entry/exit, make sure that the selected item - * is visible. The controller does this for the control so it does not have - * to listen for keyDown events, etc, on its on. - * - * Multiple layers can be defined in the controller representing a logical - * z-axis separation of elements. All controls in a layer are managed - * independently, but only one layer can be active at a time and have a - * selected item. - * - * In general, compound controls define a zone by their bounds, define any - * keys to override or selection behaviors (e.g., handling wrap-around by - * by overriding the left/right or up/down keys, and returning the left item - * when there is no item to select when the right arrow key is pressed.) - * - * The KeyController handles most behaviors automatically, including arrow - * keys, mouseenter, etc. - * - * - */ - -module.exports = function (gtv,$) { - /** - * KeyZoneCreationParams class. Holds params used to initialize a new - * KeyBehaviorZone. - * @constructor - */ - gtv.jq.KeyZoneCreationParams = function () { - }; - - /** - * jQuery selector that uniquely identifies the containing zone on the page. - * @type string - */ - gtv.jq.KeyZoneCreationParams.prototype.containerSelector = null; - - /** - * Mapping of key code numbers to callback methods. - * @type gtv.jq.KeyMapping - */ - gtv.jq.KeyZoneCreationParams.prototype.keyMapping = null; - - /** - * Callbacks for supported Key Controller actions. - * @type gtv.jq.KeyActions - */ - gtv.jq.KeyZoneCreationParams.prototype.actions = null; - - /** - * A collection of jQuery selectors for non-geometry key navigation. - * @type gtv.jq.KeyNavSelectors - */ - gtv.jq.KeyZoneCreationParams.prototype.navSelectors = null; - - /** - * CSS classes to determine how to highlight a particular selected item. - * @type gtv.jq.KeySelectionCssClasses - */ - gtv.jq.KeyZoneCreationParams.prototype.selectionClasses = null; - - /** - * Optional string that, if found in the element's jQuery.data(), will - * cause the item to be highlighted with the hasData selection class. - * @type string - */ - gtv.jq.KeyZoneCreationParams.prototype.navigableData = null; - - /** - * If true, a multi-row zone will save the selected item for each row, - * and return selection to that item when selection returns to that row. - * @type boolean - */ - gtv.jq.KeyZoneCreationParams.prototype.saveRowPosition = null; - - /** - * If true, the key controller determines the next selected item by - * examining the page and finding the closest item in the - * direction the user has navigated. - * @type boolean - */ - gtv.jq.KeyZoneCreationParams.prototype.useGeometry = null; - - /** - * If true, the key controller will move the selection to a non-visible - * element on the page. (The zone would likely supply a scrollIntoView - * callback that made the item visible when selected.) - * @type boolean - */ - gtv.jq.KeyZoneCreationParams.prototype.selectHidden = null; - - - /** - * KeyMapping class. Holds key mappings to be called by the key controller - * when a key event is received. Each entry should be numbered by the keycode - * (e.g., ENTER = 13) paired with a callback. - * @constructor - */ - gtv.jq.KeyMapping = function () { - }; - - /** - * Key mapping callbacks should be numbered by keycode, for example, passing: - * var keyMapping = { 13: enterCallback }; - * will cause the function 'enterCallback' to be called when the ENTER key - * is pressed. - * @param {jQuery.Element} selectedItem The currently selected item. May be - * undefined/null if there is no currently selected item. - * @param {jQuery.Element} newSelected A candidate item to be the next - * selected item. For movement keys (e.g., arrows) this will be set - * unless the controller cannot find a candidate item in the direction - * of the arrow movement. For other keys, it will be unset. - * @return {gtv.jq.Selection} A Selection object telling the key controller - * what to do in response to the callback (may be to ignore the result, - * skip further processing, or change the selected item; see - * gtv.jq.Selection for details). - */ - gtv.jq.KeyMapping.prototype.keyCallback = null; - - - /** - * KeyActions class. Provides the key controller zone with a set of callbacks - * to make when certain events happen in the key controller. - * @constructor - */ - gtv.jq.KeyActions = function () { - }; - - /** - * Called when a zone is entered. This is called in descending order from - * a parent to child zones. Optionally returns a the item to be selected - * after the zone is entered. This is only honored if the zone is at the - * end of the parent-child heirarchy. - * @type Function() - */ - gtv.jq.KeyActions.prototype.enterZone = null; - - /** - * Called when a zone is exited. This is called in ascending order from - * child to parent zones. - * @type Function(selectedItem) - * @param {jQuery.Element} selectedItem The selected item in the zone. - */ - gtv.jq.KeyActions.prototype.leaveZone = null; - - /** - * Called every time an item in the zone receives the selection. This method - * must determine if the selected item is in view and, if it is, position it - * such that it is completely visible on the page. If the item is already - * visible, for performance reasons it is best to do nothing and return - * immediately. - * @type Function(newZone, newSelected, syncCallback) - * @param {gtv.jq.KeyBehaviorZone} newZone The zone where the item resides. - * @param {jQuery.Element} newSelected The item to be selected. - * @param {Function} syncCallback Function to call when any movement - * animation related to moving the item into view is completed. The - * key controller will wait for these animations to complete before - * processing further event input. - */ - gtv.jq.KeyActions.prototype.scrollIntoView = null; - - /** - * Called after the selection has moved to a new item in the zone. - * @type Function(selectedItem, newSelected) - * @param {jQuery.Element} selectedItem The currently selected item (that - * selection is moving away from). - * @param {jQuery.Element} newSelected Item that selection is moving to. - */ - gtv.jq.KeyActions.prototype.moveSelected = null; - - /** - * Called when the user clicks the mouse button on an item. - * @type Function(item) - * @param {jQuery.Element} item The item clicked on. - */ - gtv.jq.KeyActions.prototype.click = null; - - - /** - * KeyNavSelectors class. - * @constructor - */ - gtv.jq.KeyNavSelectors = function () { - }; - - /** - * The jQuery selector to use to determine if an element is an item, that is, - * a navigable element on the page. - * @type string - */ - gtv.jq.KeyNavSelectors.prototype.item = null; - - /** - * The jQuery selector to use to determine if an element is the immediate - * container of an 'item' - * @type string - */ - gtv.jq.KeyNavSelectors.prototype.itemParent = null; - - /** - * The jQuery selector to use to determine if an element is the parent of - * an item parent. All itemParent children of a row are navigated horizontally, - * that is, with the left/right arrows. Rows themselves are navigated - * vertically (up/down arrows). - * @type string - */ - gtv.jq.KeyNavSelectors.prototype.itemRow = null; - - /** - * The jQuery selector to use to determine if an element represents a page of - * itemRows or itemParents. Used to segment items to support paging. - * @type string - */ - gtv.jq.KeyNavSelectors.prototype.itemPage = null; - - - /** - * KeySelectionCssClasses class. A collection of CSS classes for highlighting - * selected items of various states - * @constructor - */ - gtv.jq.KeySelectionCssClasses = function () { - }; - - /** - * Used to highlight items that do not meet the criteria for any other - * selection class. At present, this means that if an item does not have - * a jQuery.data() value for zone's navigableData string, this class will - * be used to highlight. - * @type string - */ - gtv.jq.KeySelectionCssClasses.prototype.basic = null; - - /** - * Used to highlight items that have a jQuery.data() value for the zone's - * navigableData string. That is, if navigableData is 'destUrl', then any - * element where element.data('destUrl') != undefined will be highlighted - * with this CSS class. - * @type string - */ - gtv.jq.KeySelectionCssClasses.prototype.hasData = null; - - - /** - * KeyBehaviorZone class. Defines a zone of key navigation rules. - * @param {gtv.jq.KeyZoneCreationParams} createParams Initialization data for - * the new zone. - * @constructor - */ - gtv.jq.KeyBehaviorZone = function (createParams) { - this.params = createParams; - - this.params.keyMapping = this.params.keyMapping || {}; - this.params.actions = this.params.actions || {}; - this.params.navSelectors = this.params.navSelectors || {}; - this.params.selectionClasses = this.params.selectionClasses || {}; - }; - - - /** - * Representation of the return code from key nav callback functions. - * @param {string} status One of: - * none Continue to process the event. - * skip Stop processing the event. - * selected Replace the selected item with the one returned. - * @param {jQuery.Element} selected The item to use as the newly selected item. - */ - gtv.jq.Selection = function (status, selected) { - this.status = status; - this.selected = selected; - } - - - /** - * KeyController class. Manages navigation for a page. - * @constructor - */ - gtv.jq.KeyController = function () { - this.selectedItem_ = null; - this.zoneLayers_ = []; - this.currentZone_ = null; - this.globalKeyMapping_ = {}; - this.moving_ = false; - this.started_ = false; - this.activeLayer_ = null; - - this.zoneLayers_['default'] = new gtv.jq.KeyZoneLayer_(0); - }; - - /** - * Creates a layer of the specified name and priority. If priority isn't needed, - * callers can instead rely on the implicit layer creation when adding a zone. - * @param {string} layerName The name of the layer. - * @param {?number} priority Priority of the layer (presently unused). - * @return {KeyZoneLayer_} The newly created layer. - */ - gtv.jq.KeyController.prototype.createLayer = function (layerName, priority) { - var keyController = this; - - if (keyController.zoneLayers_[layerName]) { - return keyController.zoneLayers_[layerName]; - } - - var zoneLayer = new gtv.jq.KeyZoneLayer_(priority); - keyController.zoneLayers_[layerName] = zoneLayer; - - return zoneLayer; - }; - - /** - * Deletes the named layer. If the layer is the currently active one, the - * default layer is selected as a replacement and selection is moved to the - * first zone. - * @param {string} layerName The name of the layer to delete. - * @return {boolean} False if the layer doesn't exist or the caller is trying - * to delete the default layer. True otherwise. - */ - gtv.jq.KeyController.prototype.deleteLayer = function (layerName) { - var keyController = this; - - if (!layerName || layerName == 'default') { - return false; - } - - var zoneLayer = keyController.zoneLayers_[layerName]; - if (!zoneLayer) { - return false; - } - - if (keyController.activeLayer_ == layerName) { - keyController.moveSelected_(); - - keyController.activeLayer_ = 'default'; - keyController.setZone( - keyController.zoneLayers_['default'].behaviorZones[0], - true); - } - - keyController.zoneLayers_[layerName] = null; - - return true; - }; - - /** - * Removes all layers from the Key Controller. - */ - gtv.jq.KeyController.prototype.removeAllLayers = function () { - var keyController = this; - - for (var layer in keyController.zoneLayers_) { - keyController.deleteLayer(layer); - } - }; - - /** - * Sets the specified item as the selected item. The item can be in any zone. - * @param {jQuery.Element} newSelected The item to move selection to. - * @param {Function} finishedCallback The function to call when any movement - * animations have completed. - */ - gtv.jq.KeyController.prototype.setSelected = function (newSelected, - finishedCallback) { - var keyController = this; - - var syncCallback = new gtv.jq.SynchronizedCallback(finishedCallback); - - var getCallback = syncCallback.getCallback(); - var animFinishedAction = getCallback(); - keyController.moveSelected_(null, newSelected, animFinishedAction); - - syncCallback.done(); - }; - - /** - * Makes the specified layer the active one, and moves selection to the first - * zone in that layer. - * @param {?string} opt_layerName The name of the layer to active. If not, - * supplied activates the default layer. - */ - gtv.jq.KeyController.prototype.setLayer = function (opt_layerName) { - var keyController = this; - var layerName = opt_layerName || 'default'; - var zoneLayer = keyController.zoneLayers_[layerName]; - - if (keyController.activeLayer_ != layerName) { - keyController.activeLayer_ = layerName; - keyController.setZone(zoneLayer.behaviorZones[0], true); - } - }; - - /** - * Sets a key mapping for the global key controller. - * @param {KeyMap} keyMapping The key mapping to install. - */ - gtv.jq.KeyController.prototype.setGlobalKeyMapping = function (keyMapping) { - this.globalKeyMapping_ = {}; - this.globalKeyMapping_ = keyMapping || {}; - }; - - /** - * Sets a key mapping for specified layer. - * @param {KeyMap} keyMapping The key mapping to install in the layer. - * @param {?string} opt_layerName The name of the layer to install the mapping. - * If not supplied, the default layer is used. - */ - gtv.jq.KeyController.prototype.setLayerKeyMapping = function (keyMapping, - opt_layerName) { - var keyController = this; - var layerName = opt_layerName || 'default'; - var zoneLayer = keyController.zoneLayers_[layerName]; - - zoneLayer.setKeyMapping_(keyMapping); - }; - - /** - * Adds a new zone to a layer in the key controller. - * @param {KeyBehaviorZone} zone The zone to add. - * @param {?boolean} opt_selectOnInit If true, the zone will receive the - * selection if no other zone in the controller has the selection. - * @param {?Array.} opt_layerNames The names of the layers to add the - * zone to. If not supplied, the default layer is used. - */ - gtv.jq.KeyController.prototype.addBehaviorZone = function (zone, - opt_selectOnInit, - opt_layerNames) { - var keyController = this; - var layerNames = opt_layerNames || ['default']; - var selectOnInit = opt_selectOnInit; - - for (var layer = 0; layer < layerNames.length; layer++) { - var zoneLayer = keyController.zoneLayers_[layerNames[layer]]; - if (!zoneLayer) { - zoneLayer = keyController.createLayer(layerNames[layer]); - } - - zoneLayer.behaviorZones.push(zone); - } - - keyController.attachZone_(zone, false); - zone.layers = layerNames; - - if (keyController.started_ && selectOnInit) { - keyController.setZone(zone); - } - }; - - /** - * Removes a zone from the key controller. If the active zone is removed, - * selection is moved to the next zone. - * @param {KeyBehaviorZone} zone The zone to remove. - */ - gtv.jq.KeyController.prototype.removeBehaviorZone = function (zone) { - var keyController = this; - - for (var layer = 0; layer < zone.layers.length; layer++) { - var zoneLayer = keyController.zoneLayers_[zone.layers[layer]]; - for (var i = 0; i < zoneLayer.behaviorZones.length; i++) { - if (zoneLayer.behaviorZones[i] == zone) { - zoneLayer.behaviorZones.splice(i, 1); - break; - } - } - } - if (zone == keyController.currentZone_) { - keyController.moveSelected_(); - } - - keyController.detachZone_(zone); - }; - - /** - * Installs an array of zones into a layer. Replaces any existing zones. - * @param {Array.} zones Array of zones to add. - * @param {?boolean} opt_selectOnInit If true, selects the first zone. - * @param {?Array.} opt_layerNames The layer to install the zones into. - * If not supplied, uses the default layer. - */ - gtv.jq.KeyController.prototype.setZones = function (zones, - opt_selectOnInit, - opt_layerNames) { - var keyController = this; - var selectOnInit = opt_selectOnInit; - var layerNames = opt_layerNames || ['default']; - var zoneLayer = keyController.zoneLayers_[layerName]; - - zoneLayer.behaviorZones = zones; - for (var i = 0; i < zones.length; i++) { - zones[i].layers = layerNames; - keyController.attachZone_(zones[i], false); - } - - if (selectOnInit && zones.length > 0) { - keyController.setZone(zoneLayer.behaviorZones[0]); - } - }; - - /** - * Removes all zones from a layer and returns the zones. Selection is moved to - * the next zone (thus activating a new layer), if available. - * @param {?string} opt_layerName The layer to remove the zones from. If not - * supplied, uses the default layer. - * @return {Array.} Array of zones removed. - */ - gtv.jq.KeyController.prototype.removeAllZones = function (opt_layerName) { - var keyController = this; - var layerName = opt_layerName || 'default'; - var zoneLayer = keyController.zoneLayers_[layerName]; - - for (var i = 0; i < zoneLayer.behaviorZones.length; i++) { - keyController.detachZone_(zoneLayer.behaviorZones[i]); - if (zoneLayer.behaviorZones[i] == keyController.currentZone_) { - keyController.leaveZoneHeirarchy_(keyController.selectedItem_); - } - } - - var zones = zoneLayer.behaviorZones; - zoneLayer.behaviorZones = []; - - keyController.moveSelected_(); - return zones; - }; - - /** - * Starts the Key Controller. Zones can be added to the controller both before - * and after it is started. - * @param {?KeyBehaviorZone} initialZone Optional initial zone to select. If not - * supplied, the first zone is selected. - * @param {?boolean} opt_selectOnInit If supplied, initialZone will be selected. - * @param {?string} opt_layerName If supplied, the specified layer will be the - * active layer. If both an initialZone and opt_layerName are supplied, the - * layer of initialZone will be active layer, and this value will be - * ignored. - * @return {boolean} True if the controller starts successfully. - */ - gtv.jq.KeyController.prototype.start = function (initialZone, - opt_selectOnInit, - opt_layerName) { - var keyController = this; - - if (keyController.started_) - return true; - - var layerName = opt_layerName || 'default'; - var selectOnInit = opt_selectOnInit; - if (!initialZone) { - keyController.activeLayer_ = layerName; - - initialZone = - keyController.zoneLayers_[keyController.activeLayer_].behaviorZones[0]; - } else { - keyController.currentZone_ = initialZone; - keyController.activeLayer_ = initialZone.layers[0]; - } - - $(document).bind('keydown.keycontroller', - function (e) { - keyController.keyDown_(e); - }); - - if (selectOnInit) { - var items = $(initialZone.params.containerSelector + ' ' + - initialZone.params.navSelectors.item); - items.first().mouseenter(); - } - - keyController.started_ = true; - - return true; - }; - - /** - * Stops all key controller activity. Unbinds event handlers and removes all - * layers. - */ - gtv.jq.KeyController.prototype.stop = function () { - var keyController = this; - - keyController.removeAllLayers(); - $(document).unbind('.keycontroller'); - - keyController.started_ = false; - }; - - /** - * Moves the selection to the specified zone only if there is not already a - * zone with selection. - * @param {KeyBehaviorZone} newZone The zone to move selection to. - * @param {boolean} opt_force Set the zone even if there is already an active - * zone. - * @return {boolean} True if selection moved, false otherwise. - */ - gtv.jq.KeyController.prototype.setZone = function (newZone, opt_force) { - var keyController = this; - - var zoneInLayer = false; - for (var layer = 0; layer < newZone.layers.length; layer++) { - if (keyController.activeLayer_ == newZone.layers[layer]) { - zoneInLayer = true; - break; - } - } - - if (!opt_force && keyController.currentZone_ && zoneInLayer) { - // If there's already a currentZone, we're not forcing the new zone, - // and the currentZone is in the active layer, don't set the new zone - return false; - } - - var newSelected = keyController.shiftZone_(newZone); - if (newSelected && newSelected.length > 0) { - if (!zoneInLayer) { - // Only change the active layer if the new zone does not exist - // in it. - keyController.activeLayer_ = newZone.layers[0]; - } - keyController.moveSelected_(newZone, newSelected); - } - - return true; - }; - - - /***************************************************************************/ - /*Private Properties and Methods********************************************/ - - /** - * KeyZoneLayer_ class defines a distinct layer of zones in the key controller. - * @param {?number} priority The priority of the zone. Presently unused. - * @private - * @constructor - */ - gtv.jq.KeyZoneLayer_ = function (priority) { - this.priority = priority || 0; - this.behaviorZones = new Array(); - this.globalKeyMapping = {}; - }; - - /** - * Priority of the zone, currently unused. - * @type number - * @protected - */ - gtv.jq.KeyZoneLayer_.prototype.priority = null; - - /** - * Behavior zones that are members of this layer. - * @type number - * @protected - */ - gtv.jq.KeyZoneLayer_.prototype.behaviorZones = null; - - /** - * Key mapping to use for all input in this layer. - * @type Object - * @protected - */ - gtv.jq.KeyZoneLayer_.prototype.globalKeyMapping = null; - - - /** - * Sets the key mapping for a layer. Outside callers should use the Key - * Controller's setLayerKeyMapping() - * @param {KeyMap} keyMapping The key mapping to install in the layer. - * @private - */ - gtv.jq.KeyZoneLayer_.prototype.setKeyMapping_ = function (keyMapping) { - this.globalKeyMapping = {}; - this.globalKeyMapping = keyMapping || {}; - }; - - - /** - * The item on the page that is currently selected, if any. - * @type jQuery.Element - * @private - */ - gtv.jq.KeyController.prototype.selectedItem_ = null; - - /** - * The currently active zone. The selectedItem, if set, is always in this zone. - * @type gtv.jq.KeyBehaviorZone - * @private - */ - gtv.jq.KeyController.prototype.currentZone_ = null; - - /** - * The current key mapping set for the global page. - * @type Object - * @private - */ - gtv.jq.KeyController.prototype.globalKeyMapping_ = {}; - - /** - * Tracks animation in progress. While true, animations to scroll into view - * are active, and input events are ignored. - * @type boolean - * @private - */ - gtv.jq.KeyController.prototype.moving_ = null; - - /** - * The current state of the KeyController. True if is started and listening - * for events. - * @type boolean - * @private - */ - gtv.jq.KeyController.prototype.started_ = null; - - /** - * The currently active layer. Defaults to 'default'. Only this layer will - * process input events, get the selection, etc. - * @type gtv.jq.KeyZoneLayer_ - * @private - */ - gtv.jq.KeyController.prototype.activeLayer_ = null; - - /** - * The available layers in the controller. There is always the 'default' layer. - * @type Array. - * @private - */ - gtv.jq.KeyController.prototype.zoneLayers_ = null; - - - /** - * Attaches a zone to the Key Controller, connecting events to the items in the - * container and numbering the items, rows and pages if necessary. - * @param {KeyBehaviorZone} zone The zone to attach - * @private - */ - gtv.jq.KeyController.prototype.attachZone_ = function (zone) { - var keyController = this; - - var items = $(zone.params.containerSelector + ' ' + - zone.params.navSelectors.item); - items.bind('mouseenter.keycontroller', - function (e) { - if (!keyController.moving_) { - for (var layer = 0; layer < zone.layers.length; layer++) { - if (zone.layers[layer] == keyController.activeLayer_) { - keyController.moveSelected_(null, $(this)); - break; - } - } - } - e.stopPropagation(); - }); - - items.bind('click.keycontroller', - function (e) { - if (!keyController.moving_) { - for (var layer = 0; layer < zone.layers.length; layer++) { - if (zone.layers[layer] == keyController.activeLayer_) { - keyController.click_($(this)); - break; - } - } - } - e.preventDefault(); - e.stopPropagation(); - }); - - var pages = $(zone.params.containerSelector); - if (zone.params.navSelectors.itemPage) { - if (!$(zone.params.containerSelector).is( - zone.params.navSelectors.itemPage)) { - pages = $(zone.params.containerSelector + ' ' + - zone.params.navSelectors.itemPage); - } - } - for (var i = 0; i < pages.length; i++) { - var pageRows = $(zone.params.containerSelector); - if (zone.params.navSelectors.itemRow) { - if (pages.eq(i).is(zone.params.navSelectors.itemRow)) { - pageRows = pages.eq(i); - } else { - pageRows = pages.eq(i).find(zone.params.navSelectors.itemRow); - } - } - - for (var j = 0; j < pageRows.length; j++) { - var pageRowItems = pageRows.eq(j).find(zone.params.navSelectors.item); - - for (var k = 0; k < pageRowItems.length; k++) { - if (pageRowItems.eq(k).data('index') == undefined) { - pageRowItems.eq(k).data('index', k); - } - } - } - } - }; - - /** - * Detaches a zone from the controller, unbinding event handlers from items. - * @param {KeyBehaviorZone} zone The zone to detach. - * @private - */ - gtv.jq.KeyController.prototype.detachZone_ = function (zone) { - var keyController = this; - - var items = $(zone.params.containerSelector + ' ' + - zone.params.navSelectors.item); - items.unbind('.keycontroller'); - }; - - /** - * Finds the next zone in a layer. This method will only return a zone if it is - * a leaf zone (has no children); it traverses down parent zones to find the - * leaves and selects the next leaf. - * @param {KeyBehaviorZone} zone The current zone. - * @return {KeyBehaviorZone} The next zone, or null if the layer has only one - * zone. - * @private - */ - gtv.jq.KeyController.prototype.nextZone_ = function (zone) { - var keyController = this; - var zoneLayer = keyController.zoneLayers_[keyController.activeLayer_]; - - if (zoneLayer.behaviorZones.length <= 1) { - return null; - } - - if (!zone) { - return null; - } - - var parentZones = []; - - var item = $(zone.params.containerSelector); - while (item.length) { - for (var j = 0; j < zoneLayer.behaviorZones.length; j++) { - var parentContainer = - item.parent(zoneLayer.behaviorZones[j].params.containerSelector); - - if (parentContainer.length != 0) { - parentZones.push(zoneLayer.behaviorZones[j]); - } - } - item = item.parent(); - } - - var newZone; - var zoneIndex; - for (var i = 0; i < zoneLayer.behaviorZones.length; i++) { - if (zoneLayer.behaviorZones[i] == zone) { - // This is the zone we're at, we want to find the next non-parent - zoneIndex = i; - } - else if (zoneIndex != undefined) { - // zoneIndex is set, meaning we're actively looking for next non-parent - if ($.inArray(zoneLayer.behaviorZones[i], parentZones)) { - newZone = zoneLayer.behaviorZones[i]; - break; - } - } - else if (!newZone) { - // zoneIndex not set, find first non-parent to handle wrap-around case - if ($.inArray(zoneLayer.behaviorZones[i], parentZones)) { - newZone = zoneLayer.behaviorZones[i]; - } - } - } - - return newZone || zone; - }; - - /** - * Handles the keyDown event for the document where the Key Controller is - * started. This handler will return immediately if there is no current zone - * or if there is scrollIntoView animation in progress. - * @param {Event} e The keydown event from the browser - * @private - */ - gtv.jq.KeyController.prototype.keyDown_ = function (e) { - var DIRECTIONS = { - left: [-1, 0], - up: [0, -1], - right: [1, 0], - down: [0, 1] - }; - var keyController = this; - - if (!keyController.currentZone_ || keyController.moving_) { - return; - } - - var direction; - var selectedIndex; - if (keyController.selectedItem_) { - selectedIndex = keyController.selectedItem_.data('index'); - } - var rowIndex = selectedIndex; - - var visibleSelector = ':visible'; - if (keyController.currentZone_.params.selectHidden) { - visibleSelector = ''; - } - - - // We only want to allow navigation to items that are visible. - var itemClass = - keyController.currentZone_.params.navSelectors.item + visibleSelector; - var itemParentClass = - keyController.currentZone_.params.navSelectors.itemParent + - visibleSelector; - var itemParentRowClass = - keyController.currentZone_.params.navSelectors.itemRow + - visibleSelector; - var itemParentRowPageClass = - keyController.currentZone_.params.navSelectors.itemPage + - visibleSelector; - - - var newZone; - var newSelected; - - if (keyController.selectedItem_) { - if (keyController.selectedItem_.is('input[type=text]')) { - // If the selected item is an input box, we want special arrow key - // nav code. - if (keyController.selectedItem_.get(0) == document.activeElement) { - // If the input has focus and is not empty, ignore the left/right - // arrow keys for navigation, since the user probably wants to - // move around in the input box - if (keyController.selectedItem_.val() && - (e.keyCode == 37 || e.keyCode == 39 || e.keyCode == 8)) { - return; - } - } else if (e.keyCode == 37 || e.keyCode == 39) { - // If the input does not have focus, allow navigation away from - // it, and prevent the key from reaching the input control - e.preventDefault(); - } - } else if (keyController.selectedItem_.is('select')) { - // If the selected item is a dropdown box, eat all the navigation - // codes meant for it so we can navigate away instead of being - // stuck cycling through values in the box. - if (e.keyCode >= 37 && e.keyCode <= 40) { - e.preventDefault(); - } - } - } - - switch (e.keyCode) { - case 9: - { // TAB - newZone = keyController.nextZone_(keyController.currentZone_); - e.preventDefault(); - break; - } - case 37: - { // left - if (!keyController.selectedItem_) - break; - - direction = DIRECTIONS.left; - - if (itemParentClass) { - var parent = keyController.selectedItem_.parent(); - newSelected = parent.prevAll(itemParentClass).eq(0).find(itemClass); - } else { - newSelected = keyController.selectedItem_.prevAll(itemClass).eq(0); - } - break; - } - case 38: - { // up - if (!keyController.selectedItem_) - break; - - direction = DIRECTIONS.up; - - if (!itemParentRowClass) { - break; - } - - if (keyController.currentZone_.params.saveRowPosition) { - keyController.selectedItem_ - .parents(itemParentRowClass) - .data('index', selectedIndex); - } - - var parentRow = keyController.selectedItem_.parents(itemParentRowClass); - var newRow = parentRow.prevAll(itemParentRowClass).eq(0); - - if (keyController.currentZone_.params.saveRowPosition && - newRow.length) { - rowIndex = newRow.data('index'); - } - - while (rowIndex >= 0 && (!newSelected || newSelected.length == 0)) { - newSelected = newRow.find(itemParentClass) - .eq(rowIndex) - .find(itemClass); - rowIndex -= 1; - } - break; - } - case 39: - { // right - if (!keyController.selectedItem_) { - break; - } - - direction = DIRECTIONS.right; - - if (itemParentClass) { - var _parent = keyController.selectedItem_.parent(); - newSelected = _parent.nextAll(itemParentClass).eq(0).find(itemClass); - } else { - newSelected = keyController.selectedItem_.nextAll(itemClass).eq(0); - } - break; - } - case 40: - { // down - if (!keyController.selectedItem_) { - break; - } - - direction = DIRECTIONS.down; - - if (!itemParentRowClass) { - break; - } - - if (keyController.currentZone_.params.saveRowPosition) { - keyController.selectedItem_ - .parents(itemParentRowClass) - .data('index', selectedIndex); - } - - var parentRow = keyController.selectedItem_.parents(itemParentRowClass); - var newRow = parentRow.nextAll(itemParentRowClass).eq(0); - - if (keyController.currentZone_.params.saveRowPosition && - newRow.length) { - rowIndex = newRow.data('index'); - } - - while (rowIndex >= 0 && (!newSelected || newSelected.length == 0)) { - newSelected = newRow.find(itemParentClass) - .eq(rowIndex) - .find(itemClass); - rowIndex -= 1; - } - break; - } - } - - if (keyController.currentZone_.params.useGeometry && - e.keyCode >= 37 && e.keyCode <= 40) { - newSelected = keyController.nearestElement_(keyController.currentZone_, - keyController.selectedItem_, - direction); - } - - var keyAction; - // If there's a global mapping action for this key, call it. - keyAction = keyController.globalKeyMapping_[e.keyCode]; - if (keyAction) { - e.preventDefault(); - var result = keyAction(keyController.selectedItem_, newSelected); - if (result.status == 'skip') { - return; - } else if (result.status == 'selected') { - newSelected = result.selected; - } - } - - // If there's a layer mapping action for this key, call it. - keyAction = keyController.zoneLayers_[keyController.activeLayer_] - .globalKeyMapping[e.keyCode]; - if (keyAction) { - e.preventDefault(); - var result = keyAction(keyController.selectedItem_, newSelected); - if (result.status == 'skip') { - return; - } else if (result.status == 'selected') { - newSelected = result.selected; - } - } - - // If the zone has a mapped action for this key, call it. - keyAction = keyController.currentZone_.params.keyMapping[e.keyCode]; - if (keyAction) { - var result = keyAction(keyController.selectedItem_, newSelected); - if (result.status == 'skip') { - return; - } else if (result.status == 'selected') { - newSelected = result.selected; - } - } - - - keyController.processSelection_(newZone, newSelected, direction); - }; - - /** - * Interprets the results of keyDown processing and moves the selection as - * appropriate. May shift to a new zone if a new zone is provided or if the - * newly selected item is unset and a directional key was pressed. - * @param {KeyBehaviorZone} newZone The new zone to be selected. If this is - * null and newSelected is null and direction is specified, this method - * will look for a new zone in the direction of movement. Otherwise, it - * will assume the current zone. - * @param {jQuery.Element} newSelected The new item to move selection to. If - * null and direction is set, the value will be obtained either by moving - * into a proximate zone (if newZone is null) or by entering newZone. - * @param {Array.} direction A two number array specifying the - * direction of movement, if any. The first number represents horizontal - * direction, the second vertical direction. - * @private - */ - gtv.jq.KeyController.prototype.processSelection_ = function (newZone, - newSelected, - direction) { - var keyController = this; - - // If the selection resulted in a change of zone or the movement - // resulted in no new selected item, try moving to a new zone. - if (newZone || - ((!newSelected || newSelected.length == 0) && direction)) { - - - if (!newZone) { - newZone = keyController.getNewZone_(keyController.selectedItem_, - direction); - } - - - if (newZone) { - if ($(newZone.params.containerSelector).is(":visible")) - newSelected = keyController.shiftZone_(newZone); - - } - } else { - - newZone = keyController.currentZone_; - } - - // If after all that we have a new item to select, move the selection. - if (newSelected && newSelected.length > 0) { - keyController.moveSelected_(newZone, newSelected); - } - }; - - /** - * Leaves the current zone, calling a leaveZone action if the zone defines one, - * otherwise, sets the zone's last selected item to the specified selectedItem. - * @param {KeyBehaviorZone} zone The zone to leave. - * @param {jQuery.Element} selectedItem The item in the zone currently selected. - * @private - */ - gtv.jq.KeyController.prototype.leaveZone_ = function (zone, selectedItem) { - if (zone.params.actions.leaveZone) { - zone.params.actions.leaveZone(selectedItem); - } else { - zone.lastSelected = selectedItem; - } - }; - - /** - * Leaves a heirarchy of zones, that is, a zone with the current selection - * and then each parent zone in turn until there are no more parent zones. - * This ensures that the heirarchy of leaveZone actions are called when a - * child zone is left. - * @param {jQuery.Element} selectedItem Child zone item currently selected. - * @private - */ - gtv.jq.KeyController.prototype.leaveZoneHeirarchy_ = function (selectedItem) { - var keyController = this; - var zoneLayer = keyController.zoneLayers_[keyController.activeLayer_]; - - var item = selectedItem; - while (item.length) { - for (var i = 0; i < zoneLayer.behaviorZones.length; i++) { - var parentContainer = - item.parent(zoneLayer.behaviorZones[i].params.containerSelector); - - if (parentContainer.length != 0) { - keyController.leaveZone_(zoneLayer.behaviorZones[i], selectedItem); - selectedItem = null; - } - } - item = item.parent(); - } - }; - - /** - * Enters the specified zone, calling the enterZone action if supplied. - * @param {KeyBehaviorZone} zone The zone to enter. - * @return {jQuery.Element} The item to select in the new zone as returned by - * the zone's enterZone action, or null if none. - * @private - */ - gtv.jq.KeyController.prototype.enterZone_ = function (zone) { - if (zone.params.actions.enterZone) { - return zone.params.actions.enterZone(); - } - - return null; - }; - - /** - * Enters a zone heirarchy, ensuring that the parents of a zone have their - * enterZone actions called when a child zone is entered. - * @param {jQuery.Element} selectedItem The iten to select in the child zone. - * @private - */ - gtv.jq.KeyController.prototype.enterZoneHeirarchy_ = function (selectedItem) { - var keyController = this; - var zoneLayer = keyController.zoneLayers_[keyController.activeLayer_]; - - var item = selectedItem; - while (item.length) { - for (var i = 0; i < zoneLayer.behaviorZones.length; i++) { - var parentContainer = - item.parent(zoneLayer.behaviorZones[i].params.containerSelector); - - if (parentContainer.length != 0) { - keyController.enterZone_(zoneLayer.behaviorZones[i]); - } - } - item = item.parent(); - } - }; - - /** - * Leaves the current zone, if any. - * @private - */ - gtv.jq.KeyController.prototype.leaveCurrentZone_ = function () { - var keyController = this; - - if (!keyController.currentZone_) { - return; - } - - keyController.leaveZoneHeirarchy_(keyController.selectedItem_); - }; - - /** - * Shifts from the current zone to a new zone and determines the item to be - * selected. - * @param {KeyBehaviorZone} newZone The new zone to enter. - * @param {jQuery.Element} newSelected The item to be selected in the zone. - * @return {jQuery.Element} The new item to selected. - * @private - */ - gtv.jq.KeyController.prototype.shiftZone_ = function (newZone, newSelected) { - var keyController = this; - - if (newZone.params.actions.enterZone) { - // If the new zone has an enterZoneAction, call it to enter the zone - newSelected = newZone.params.actions.enterZone(); - } else if (newZone.lastSelected && - $.contains($(newZone.params.containerSelector).get(0), - newZone.lastSelected.get(0))) { - // If the new zone has lastSelected set and the lastSelected item is - // still in that zone's container, set it as selected. - newSelected = newZone.lastSelected; - } - - // If there was no zone enter action, or it didn't select a new item, - // select one naively, on its behalf. - if (!newSelected || newSelected.length == 0) { - newSelected = - $(newZone.params.containerSelector + ' ' + - newZone.params.navSelectors.item).first(); - } - - return newSelected; - }; - - /** - * Finds the closest zone in the direction of movement from a specified item, - * and returns it. - * @param {jQuery.Element} fromItem The item selection is moving from. - * @param {Array.} direction The direction of movement. - * @return {KeyBehaviorZone} The best choice for the zone in the direction - * of movement, or null if there are none. - * @private - */ - gtv.jq.KeyController.prototype.getNewZone_ = function (fromItem, direction) { - var keyController = this; - var zoneLayer = keyController.zoneLayers_[keyController.activeLayer_]; - - var minZoneDistance; - var newZone; - - for (var i = 0; i < zoneLayer.behaviorZones.length; i++) { - var zone = zoneLayer.behaviorZones[i]; - - if (zone == keyController.currentZone_ || !($(zone.params.containerSelector).is(":visible"))) { - continue; - } - - var zoneContainer = $(zone.params.containerSelector); - var zoneDistance = - keyController.calcElementDistance_(fromItem, zoneContainer, direction); - - if (zoneDistance >= 0 && - (minZoneDistance == undefined || zoneDistance < minZoneDistance)) { - minZoneDistance = zoneDistance; - newZone = zone; - } - } - - return newZone; - }; - - /** - * Determines the visually "nearest" item in the zone, as laid out on the page, - * in the direction of movement. - * @param {KeyBehaviorZone} zone The zone to check. - * @param {jQuery.Element} fromItem The item being moved from. - * @param {Array.} direction The direction of movement. - * @return {jQuery.Element} The item, or null if none qualify. - * @private - */ - gtv.jq.KeyController.prototype.nearestElement_ = function (zone, - fromItem, - direction) { - var keyController = this; - - var minCheckItemDistance; - var newCheckItem; - - var visibleSelector = ':visible'; - if (zone.params.selectHidden) { - visibleSelector = ''; - } - - - var items = $(zone.params.containerSelector + ' ' + - zone.params.navSelectors.item + visibleSelector); - var r,il; - il=items.length; - for (var i = 0; i < il; i++) { - var checkItem = items.eq(i); - if (checkItem.get(0) == fromItem.get(0)) { - continue; - } - - var checkItemDistance = - keyController.calcElementDistance_(fromItem, checkItem, direction); - - if (checkItemDistance > minCheckItemDistance){ - continue; - } - if (checkItemDistance >= 0 && - (minCheckItemDistance == undefined || - checkItemDistance < minCheckItemDistance)) { - minCheckItemDistance = checkItemDistance; - r=i; - } - } - newCheckItem=items.eq(r); - return newCheckItem; - }; - - /** - * Calculates a weighted Euclidean distance between two elements on the page - * in a given direction. - * @param {jQuery.Element} fromItem The start element. - * @param {jQuery.Element} toItem The destination element. - * @param {Array.} direction The direction of movement. - * @return {number} The weighted Euclidean distance, or -1 if the toItem is - * not in the specified direction. - * @private - */ - gtv.jq.KeyController.prototype.calcElementDistance_ = function (fromItem, - toItem, - direction) { - function calcDistance(dx, dy) { - return Math.floor(Math.sqrt((dx * dx) + (dy * dy))); - } - - function calcOffset(elt) { - var rect = elt.getBoundingClientRect(), bodyElt = document.body; - return { - top: rect.top + bodyElt.scrollTop, - left: rect.left + bodyElt.scrollLeft - } - } - - - var nativeFromItem=fromItem.get(0); - var fromItemOffset = calcOffset(nativeFromItem); - var fromItemLeft = fromItemOffset.left; - var fromItemTop = fromItemOffset.top; - var fromItemRight = fromItemLeft + nativeFromItem.offsetWidth; - var fromItemBottom = fromItemTop + nativeFromItem.offsetHeight; - var fromItemCenterX = fromItemLeft + (nativeFromItem.offsetWidth / 2); - var fromItemCenterY = fromItemTop + (nativeFromItem.offsetHeight / 2); - - var nativeToItem=toItem.get(0); - var toItemOffset = calcOffset(nativeToItem); - var toItemLeft = toItemOffset.left; - var toItemTop = toItemOffset.top; - var toItemRight = toItemLeft + nativeToItem.offsetWidth; - var toItemBottom = toItemTop + nativeToItem.offsetHeight; - var toItemCenterX = toItemLeft + (nativeToItem.offsetWidth / 2); - var toItemCenterY = toItemTop + (nativeToItem.offsetHeight / 2); - - var toItemDistance; - var distanceX; - var distanceY; - - if (direction[1] == 0) { - if (direction[0] < 0) { - if (toItemRight <= fromItemLeft) { - distanceX = fromItemLeft - toItemRight; - } - - if (toItemCenterX <= fromItemLeft) { - if (distanceX != undefined) { - distanceX = Math.min(distanceX, fromItemLeft - toItemCenterX); - } else { - distanceX = fromItemLeft - toItemCenterX; - } - } - - if (toItemRight <= fromItemLeft) { - if (distanceX != undefined) { - distanceX = Math.min(distanceX, fromItemLeft - toItemRight); - } else { - distanceX = fromItemLeft - toItemRight; - } - } - } else { - if (fromItemRight <= toItemLeft) { - distanceX = toItemLeft - fromItemRight; - } - - if (fromItemRight <= toItemCenterX) { - if (distanceX != undefined) { - distanceX = Math.min(distanceX, toItemCenterX - fromItemRight); - } else { - distanceX = toItemCenterX - fromItemRight; - } - } - - if (fromItemLeft < toItemLeft) { - if (distanceX != undefined) { - distanceX = Math.min(distanceX, toItemLeft - fromItemLeft); - } else { - distanceX = toItemLeft - fromItemLeft; - } - } - } - - distanceY = Math.min(Math.abs(fromItemCenterY - toItemTop), - Math.abs(fromItemCenterY - toItemCenterY), - Math.abs(fromItemCenterY - toItemBottom)) * 2; - } else if (direction[0] == 0) { - if (direction[1] < 0) { - if (toItemBottom <= fromItemTop) { - distanceY = fromItemTop - toItemBottom; - } - - if (toItemCenterY <= fromItemTop) { - if (distanceY != undefined) { - distanceY = Math.min(distanceY, fromItemTop - toItemCenterY); - } else { - distanceY = fromItemTop - toItemCenterY; - } - } - - if (toItemBottom <= fromItemTop) { - if (distanceY != undefined) { - distanceY = Math.min(distanceY, fromItemTop - toItemBottom); - } else { - distanceY = fromItemTop - toItemBottom; - } - } - } else { - if (fromItemBottom <= toItemTop) { - distanceY = toItemTop - fromItemBottom; - } - - if (fromItemBottom <= toItemCenterY) { - if (distanceY != undefined) { - distanceY = Math.min(distanceY, toItemCenterY - fromItemBottom); - } else { - distanceY = toItemCenterY - fromItemBottom; - } - } - - if (fromItemTop < toItemTop) { - if (distanceY != undefined) { - distanceY = Math.min(distanceY, toItemTop - fromItemTop); - } else { - distanceY = toItemTop - fromItemTop; - } - } - } - - distanceX = Math.min(Math.abs(fromItemCenterX - toItemLeft), - Math.abs(fromItemCenterX - toItemCenterX), - Math.abs(fromItemCenterX - toItemRight)) * 2; - } - - // If either distance is undefined, the toItem is in the wrong direction, - // so forget trying to move to it. - if (distanceX == undefined || distanceY == undefined) { - toItemDistance = -1; - } else { - toItemDistance = calcDistance(distanceX, distanceY); - } - - return toItemDistance; - }; - - /** - * Moves selection from the current selected element to the specified element, - * which may be in a new zone. The selectedItem may be null. - * @param {KeyBehaviorZone} newZone The new zone to move selection to. - * @param {jQuery.Element} newSelected The new item to select. If not supplied, - * this method assumes that selection is leaving the current zone. - * @param {SynchronizedCallback.acquireCallback} animFinishedCallback Reference - * counting callback function used to track when all scrollIntoView - * animations are completed. - * @private - */ - gtv.jq.KeyController.prototype.moveSelected_ = function (newZone, - newSelected, - animFinishedAction) { - var keyController = this; - var zoneLayer = keyController.zoneLayers_[keyController.activeLayer_]; - - function finishedCallback() { - keyController.moving_ = false; - if (animFinishedAction) { - animFinishedAction(); - } - } - - var syncCallback = new gtv.jq.SynchronizedCallback(finishedCallback); - keyController.moving_ = true; - - if (keyController.selectedItem_ && - keyController.selectedItem_ != newSelected) { - if (keyController.currentZone_.params.selectionClasses.basic) { - if (keyController.selectedItem_.hasClass(keyController.currentZone_.params.selectionClasses.basic)) { - keyController.selectedItem_.removeClass( - keyController.currentZone_.params.selectionClasses.basic); - } - } - - if (keyController.currentZone_.params.selectionClasses.hasData) { - if (keyController.selectedItem_.hasClass(keyController.currentZone_.params.selectionClasses.hasData)) { - keyController.selectedItem_.removeClass( - keyController.currentZone_.params.selectionClasses.hasData); - } - } - - if (keyController.selectedItem_.blur) { - keyController.selectedItem_.blur(); - } - } - - if (newSelected) { - var findContainer; - if (newZone) { - findContainer = newSelected.parents(newZone.params.containerSelector); - } - - if (!findContainer || findContainer.length == 0) { - for (var i = 0; i < zoneLayer.behaviorZones.length; i++) { - findContainer = newSelected.parents( - zoneLayer.behaviorZones[i].params.containerSelector); - - if (findContainer.length) { - newZone = zoneLayer.behaviorZones[i]; - keyController.shiftZone_(newZone, newSelected); - break; - } - } - } - - var shouldLeaveZone = true; - if (newZone == keyController.currentZone_) { - shouldLeaveZone = false; - } - - function findChildZones(parent) { - var j; - for (j = 0; j < zoneLayer.behaviorZones.length; j++) { - var childContainer = - parent.children(zoneLayer.behaviorZones[j].params.containerSelector); - - if (childContainer.length > 0) { - newZone = zoneLayer.behaviorZones[j]; - newSelected = keyController.shiftZone_(newZone); - if (newZone == keyController.currentZone_) - shouldLeaveZone = false; - - findChildZones(newSelected); - return true; - } - } - - var children = parent.children(); - for (j = 0; j < children.length; j++) { - if (findChildZones(children)) { - return true; - } - } - - return false; - } - - keyController.enterZoneHeirarchy_(newSelected); - findChildZones(newSelected); - - if (shouldLeaveZone) { - keyController.leaveCurrentZone_(); - } - - keyController.scrollIntoView_(newZone, - newSelected, - syncCallback); - - if (newZone.params.navigableData && - newZone.params.selectionClasses.hasData && - newSelected.data(newZone.params.navigableData)) { - newSelected.addClass(newZone.params.selectionClasses.hasData); - newSelected.focus(); - } else if (newZone.params.selectionClasses.basic) { - newSelected.addClass(newZone.params.selectionClasses.basic); - newSelected.focus(); - } - } else { - // If no item is selected, we must be leaving the current zone - keyController.leaveCurrentZone_(); - } - - if (newZone && newZone.params.actions.moveSelected) { - newZone.params.actions.moveSelected( - keyController.selectedItem_, - newSelected); - } - - keyController.selectedItem_ = newSelected; - keyController.currentZone_ = newZone; - if (newZone) { - // Look to see if this new zone has presence in the current layer. - for (var layer = 0; layer < newZone.layers.length; layer++) { - if (newZone.layers[layer] == keyController.activeLayer_) { - break; - } - } - - // If we didn't find the active layer in the layers the zone has - // has presence in, then set the active layer to the new zone's first. - if (layer == newZone.layers.length) { - keyController.activeLayer_ = newZone.layers[0]; - } - } - - syncCallback.done(); - }; - - /** - * Calls the zone's scrollIntoView action, if any. - * @param {KeyBehaviorZone} zone The zone containing the item. - * @param {jQuery.Element} item Item the action should make sure is visible. - * @param {SynchronizedCallback} syncCallback The synchronized callback object - * that will wait for the scroll animations to complete. - * @private - */ - gtv.jq.KeyController.prototype.scrollIntoView_ = function (zone, - item, - syncCallback) { - var keyController = this; - - var container = $(item).parents(zone.params.navSelectors.itemParent); - - if (zone.params.actions.scrollIntoView) { - zone.params.actions.scrollIntoView(keyController.selectedItem_, - item, - syncCallback.getCallback()); - } - }; - - /** - * Handler for mouse clicks on any item managed by the Key Controller. - * @param {jQuery.Element} item The item receiving the click. - * @private - */ - gtv.jq.KeyController.prototype.click_ = function (item) { - var keyController = this; - - if (keyController.currentZone_) { - if (keyController.currentZone_.params.actions.click) { - keyController.currentZone_.params.actions.click(item); - } - } - }; -}; diff --git a/lib/gtv/sidenav.js b/lib/gtv/sidenav.js deleted file mode 100644 index bba83cf..0000000 --- a/lib/gtv/sidenav.js +++ /dev/null @@ -1,468 +0,0 @@ -// Copyright 2010 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. - -/** - * @fileoverview Classes for SideNavControl - * - */ - - -/** - * SideNavParams class holds configuration values specific to SideNav. - * @param {gtv.jq.SideNavParams} opt_params Optional initialization values. - * @constructor - */ -module.exports=function(gtv,$){ - -gtv.jq.SideNavParams = function (opt_params) { - var params = opt_params || {}; - - this.behaviors = new gtv.jq.SideNavBehaviors(params.behaviors); - this.createParams = new gtv.jq.CreationParams(params.createParams); -}; - -/** - * CreationParams for the SideNav control. - * @type CreationParams - */ -gtv.jq.SideNavParams.prototype.createParams = null; - -/** - * Behaviors for the SideNav control. - * @type SideNavBehaviors - */ -gtv.jq.SideNavParams.prototype.behaviors = null; - - -/** - * SideNavBehaviors configures the behaviors for a SideNav control. - * @param {gtv.jq.SideNavBehaviors} opt_behaviors Optional initial values. - * @constructor - */ -gtv.jq.SideNavBehaviors = function (opt_behaviors) { - var behaviors = opt_behaviors || {}; - - this.orientation = behaviors.orientation || 'vertical'; - this.popOut = behaviors.popOut || ''; - this.fade = behaviors.fade || ''; - this.selectOnInit = behaviors.selectOnInit || false; -}; - -/** - * Tells the SideNav control how to orient itself: 'vertical' (default) or - * 'horizontal' - * @type string - */ -gtv.jq.SideNavBehaviors.prototype.orientation; - -/** - * Tells the SideNav control to pop out from a side. One of: 'left', 'right', - * 'top', 'bottom'. - * @type string - */ -gtv.jq.SideNavBehaviors.prototype.popOut; - -/** - * If true, tells the SideNav control to fade in/out when selection moves to it. - * @type boolean - */ -gtv.jq.SideNavBehaviors.prototype.fade; - -/** - * If true, the first item in the SideNav menu will be 'chosen' when the - * control is shown. - * @type boolean - */ -gtv.jq.SideNavBehaviors.prototype.selectOnInit; - - -/** - * SideNavControl class. SideNav control is a pop-out or fade-in menu control - * that can manage selection or contain other controls. - * @param {gtv.jq.SideNavParams} sidenavParams - * @constructor - */ -gtv.jq.SideNavControl = function (sidenavParams) { - this.params_ = new gtv.jq.SideNavParams(sidenavParams); -}; - -/** - * Parent element containing the control elements. - * @type jQuery.Element - * @private - */ -gtv.jq.SideNavControl.prototype.container_ = null; - -/** - * Collection of menu-item rows in the sidenav control. - * @type Array. - * @private - */ -gtv.jq.SideNavControl.prototype.rows_ = null; - -/** - * Holds the params the control was created with. - * @type CreationParams - * @private - */ -gtv.jq.SideNavControl.prototype.params_ = null; - -/** - * Holds the params for showing the control. - * @type ShowParams - * @private - */ -gtv.jq.SideNavControl.prototype.showParams_ = null; - -/** - * Key controller behavior zone for this control. - * @type KeyBehaviorZone - * @private - */ -gtv.jq.SideNavControl.prototype.behaviorZone_ = null; - -/** - * Moves selection to the SideNav Control. - */ -gtv.jq.SideNavControl.prototype.selectControl = function () { - var sideNavControl = this; - - sideNavControl.params_.createParams.keyController.setZone( - sideNavControl.behaviorZone_, - true); -}; - -/** - * Removes the control from its container and its key control zone. - */ -gtv.jq.SideNavControl.prototype.deleteControl = function () { - var sideNavControl = this; - - sideNavControl.params_.createParams.keyController.removeBehaviorZone( - sideNavControl.behaviorZone_); - sideNavControl.container_.remove(); -}; - -/** - * Creates a new SideNavControl with the specified items and adds it to a - * container_ on the page. - * @param {gtv.jq.ShowParams} showParams Params for creating the control. - */ -gtv.jq.SideNavControl.prototype.showControl = function (showParams) { - var sideNavControl = this; - - sideNavControl.showParams_ = new gtv.jq.ShowParams(showParams); - - sideNavControl.container_ = $('
'); - sideNavControl.container_.addClass('sidenav-container') - .attr('id', sideNavControl.params_.createParams.containerId); - sideNavControl.showParams_.topParent.append(sideNavControl.container_); - - sideNavControl.rows_ = $('
').addClass('sidenav-rows'); - sideNavControl.container_.append(sideNavControl.rows_); - - var addNextItem = - gtv.jq.GtvCore.makeAddNextItemParams(sideNavControl.showParams_.contents); - if (!addNextItem) { - throw new Error('SideNavControl requires either items or itemsGenerator'); - } - - var firstItem; - var j = 0; - // This loop adds items to the sidenav menu. It continues to add until the - // addNextItem() function (generated above) returns false, signalling that - // no new items are available to add. - while (true) { - var itemRow; - if (!itemRow || - sideNavControl.params_.behaviors.orientation == 'vertical') { - itemRow = $('
').addClass('sidenav-item-row ' + - sideNavControl.params_.createParams.styles.row); - sideNavControl.rows_.append(itemRow); - } - - var itemDiv = $('
').addClass('sidenav-item-div ' + - sideNavControl.params_.createParams.styles.itemDiv); - if (sideNavControl.params_.behaviors.orientation == 'horizontal') - itemDiv.css('float', 'left'); - itemRow.append(itemDiv); - - var item = $('
') - .addClass('sidenav-item ' + - sideNavControl.params_.createParams.styles.normal - + ' ' + sideNavControl.params_.createParams.styles.item) - .data('index', j); - itemDiv.append(item); - - if (!firstItem) - firstItem = item; - - if (!addNextItem(item)) { - itemDiv.remove(); - if (sideNavControl.params_.behaviors.orientation == 'vertical') - itemRow.remove(); - break; - } - - j++; - } - - sideNavControl.setBehaviors(sideNavControl.params_.behaviors, true); - - var keyMapping = { - // enter key calls the chosenAction callback provided by the control client. - 13: function (selectedItem, newSelected) { - sideNavControl.handleChosenAction_(selectedItem); - return new gtv.jq.Selection('skip'); - } - }; - var navSelectors = { - item: '.sidenav-item', - itemParent: '.sidenav-item-div', - itemRow: '.sidenav-item-row' - }; - var selectionClasses = { - basic: sideNavControl.params_.createParams.styles.selected - }; - var actions = { - // click calls the chosenAction callback provided by the control client. - click: function (selectedItem, newItem) { - sideNavControl.handleChosenAction_(selectedItem); - }, - // When entering the zone for this control, animate the nav bar into view - // as appropriate (scroll in from sides, or fade in) - enterZone: function () { - return sideNavControl.handleEnterZone_(); - }, - // When leaving the zone for this control, animate the nav bar out of view - // as appropriate (scroll out to sides, or fade out) - leaveZone: function () { - sideNavControl.handleLeaveZone_(); - } - }; - - sideNavControl.behaviorZone_ = - new gtv.jq.KeyBehaviorZone({ - containerSelector: '#' + - sideNavControl.params_.createParams.containerId, - keyMapping: keyMapping, - actions: actions, - navSelectors: navSelectors, - selectionClasses: selectionClasses - }); - - sideNavControl.params_.createParams.keyController.addBehaviorZone( - sideNavControl.behaviorZone_, - true, - sideNavControl.params_.createParams.layerNames); - - if (sideNavControl.params_.behaviors.selectOnInit) { - sideNavControl.handleChosenAction_(firstItem); - } -}; - -/** - * Shows the control, if approprate, when the zone for this control is - * entered. - * @return {jQuery.Element} The item in the zone that should be selected - * upon entry. In this case, we start with the chosen nav item. - * @private - */ -gtv.jq.SideNavControl.prototype.handleEnterZone_ = function () { - var sideNavControl = this; - - if (sideNavControl.params_.behaviors.popOut) { - sideNavControl.container_.css({ - '-webkit-transition': 'all 1s ease-in-out' - }); - if (sideNavControl.params_.behaviors.popOut == 'left') { - sideNavControl.container_.css({ - left: '0px' - }); - } else if (sideNavControl.params_.behaviors.popOut == 'right') { - var windowWidth = $(window).width(); - var width = sideNavControl.container_.outerWidth(true); - sideNavControl.container_.css({ - left: (windowWidth - width) + 'px' - }); - } else if (sideNavControl.params_.behaviors.popOut == 'top') { - sideNavControl.container_.css({ - top: '0px' - }); - } else if (sideNavControl.params_.behaviors.popOut == 'bottom') { - var windowHeight = $(window).height(); - var height = sideNavControl.container_.outerHeight(true); - sideNavControl.container_.css({ - top: (windowHeight - height) + 'px' - }); - } - } else if (sideNavControl.params_.behaviors.fade) { - sideNavControl.container_.css({ - '-webkit-transition': 'all 1s ease-in-out' - }); - - sideNavControl.container_.css({ - opacity: '1.0' - }); - } - return sideNavControl.chosenItem; -}; - -/** - * Hides the control, if approprate, when the zone for this control is - * exited. - * @private - */ -gtv.jq.SideNavControl.prototype.handleLeaveZone_ = function () { - var sideNavControl = this; - - if (sideNavControl.params_.behaviors.popOut) { - sideNavControl.container_.css({ - '-webkit-transition': 'all 1s ease-in-out' - }); - if (sideNavControl.params_.behaviors.popOut == 'left') { - var width = sideNavControl.container_.outerWidth(true); - sideNavControl.container_.css({ - left: (-width) + 'px' - }); - } else if (sideNavControl.params_.behaviors.popOut == 'right') { - var windowWidth = $(window).width(); - sideNavControl.container_.css({ - left: (windowWidth) + 'px' - }); - } else if (sideNavControl.params_.behaviors.popOut == 'top') { - var height = sideNavControl.container_.outerHeight(true); - sideNavControl.container_.css({ - top: (-height) + 'px' - }); - } else if (sideNavControl.params_.behaviors.popOut == 'bottom') { - var windowHeight = $(window).height(); - sideNavControl.container_.css({ - top: (windowHeight) + 'px' - }); - } - } else if (sideNavControl.params_.behaviors.fade) { - sideNavControl.container_.css({ - '-webkit-transition': 'all 1s ease-in-out' - }); - sideNavControl.container_.css({ - opacity: '0' - }); - } -}; - - -/** - * Applies a new set of behaviors to the control. - * @param {Object} behaviors New behaviors to apply to the control. - * @param {boolean} force Set the new behaviors even if identical. - */ -gtv.jq.SideNavControl.prototype.setBehaviors = function (behaviors, force) { - var sideNavControl = this; - - if (!force && behaviors === sideNavControl.params_.behaviors) { - return; - } - - sideNavControl.params_.behaviors = behaviors || {}; - - var height = sideNavControl.rows_.height(); - var width = sideNavControl.rows_.width(); - sideNavControl.container_.css({ - height: height + 'px', - width: width + 'px' - }); - - if (behaviors.popOut) { - // If this is a popout nav bar from the side, position the container at - // its starting point off the page, based on the side of the page it is - // popping out of. - if (behaviors.popOut == 'left') { - var containerWidth = sideNavControl.container_.outerWidth(true); - sideNavControl.container_.css({ - position: 'absolute', - left: (-containerWidth) + 'px' - }); - } else if (behaviors.popOut == 'right') { - var windowWidth = $(window).width(); - sideNavControl.container_.css({ - position: 'absolute', - left: (windowWidth) + 'px' - }); - } else if (behaviors.popOut == 'top') { - var containerHeight = sideNavControl.container_.outerHeight(true); - sideNavControl.container_.css({ - position: 'absolute', - top: (-containerHeight) + 'px' - }); - } else if (behaviors.popOut == 'bottom') { - var windowHeight = $(window).height(); - sideNavControl.container_.css({ - position: 'absolute', - top: (windowHeight) + 'px' - }); - } - - // Add a semi-opaque backdrop under the nav var when its popped out for - // visual clarity. - var backdrop = sideNavControl.container_.children('.sidenav-backdrop'); - if (backdrop.length == 0) { - backdrop = $('
').addClass('sidenav-backdrop'); - sideNavControl.container_.prepend(backdrop); - } - - sideNavControl.rows_.css('position', 'absolute'); - } else if (behaviors.fade) { - // If this is a fade-in nav bar, set its opacity to 0 to start. - sideNavControl.rows_.css('position', 'absolute'); - sideNavControl.container_.css({ - opacity: '0' - }); - } else { - // If the nav menu is neither popout or fade-in, it's statically positioned. - var backdrop = sideNavControl.container_.find('.sidenav-backdrop'); - backdrop.remove(); - sideNavControl.rows_.css('position', 'static'); - } -}; - -/** - * Event handler for click or keypress. Moves the chosen style if - * supplied, and makes choice callback if supplied. - * @param {jQuery.Element} selectedItem The item that received the event. - * @private - */ -gtv.jq.SideNavControl.prototype.handleChosenAction_ = function (selectedItem) { - var sideNavControl = this; - - if (sideNavControl.params_.createParams.styles.chosen) { - if (sideNavControl.chosenItem) { - sideNavControl.chosenItem.removeClass( - sideNavControl.params_.createParams.styles.chosen); - sideNavControl.chosenItem.addClass( - sideNavControl.params_.createParams.styles.normal); - } - selectedItem.removeClass(sideNavControl.params_.createParams.styles.normal); - selectedItem.addClass(sideNavControl.params_.createParams.styles.chosen); - } - - sideNavControl.chosenItem = selectedItem; - - if (sideNavControl.params_.createParams.choiceCallback) { - sideNavControl.params_.createParams.choiceCallback(selectedItem); - } -}; - - -}; diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 0ec708c..0000000 --- a/lib/index.js +++ /dev/null @@ -1,6 +0,0 @@ -'use strict'; -var $ = require('jquery'); -var gtvcore=require('./gtv/gtvcore')($); -require('./gtv/keycontrol')(gtvcore,$); -require('./gtv/sidenav')(gtvcore,$); -module.exports = gtvcore; diff --git a/package.json b/package.json index 0df15c8..00e5a20 100644 --- a/package.json +++ b/package.json @@ -1,47 +1,23 @@ { - "name": "gtvzone", - "version": "0.0.6", - "description": "Jquery google tv adapcion, libreria para desarrollo de aplicaciones para smart-TV", - "homepage": "https://github.com/renearias/gtvzone#readme", - "author": "renearias ", - "files": [ - "lib" - ], - "main": "lib/index.js", - "keywords": [ - "smart", - "tv", - "google", - "tv", - "mapping", - "navigation" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/renearias/gtvzone.git" - }, - "devDependencies": { - "gulp": "^3.6.0", - "gulp-eslint": "^1.0.0", - "gulp-exclude-gitignore": "^1.0.0", - "gulp-istanbul": "^0.9.0", - "gulp-mocha": "^2.0.0", - "gulp-plumber": "^1.0.0", - "gulp-nsp": "^2.1.0" + "name": "gtvzone-monorepo", + "private": true, + "description": "Navegación espacial para Smart TV: core agnóstico + bindings para DOM, React y Angular", + "license": "MIT", + "packageManager": "pnpm@11.1.2", + "engines": { + "node": ">=20" }, "scripts": { - "prepublish": "gulp prepublish", - "test": "gulp" + "build": "pnpm -r --workspace-concurrency=1 run build", + "test": "vitest run", + "test:watch": "vitest", + "typecheck": "pnpm -r run typecheck" }, - "license": "MIT", - "dependencies": { - "jquery": "^2.1.4" - }, - "bugs": { - "url": "https://github.com/renearias/gtvzone/issues" + "devDependencies": { + "jsdom": "^27.0.0", + "tsup": "^8.5.1", + "typescript": "~6.0.3", + "vitest": "^4.0.0" }, - "directories": { - "lib": "lib", - "test": "test" - } + "type": "module" } diff --git a/packages/angular/README.md b/packages/angular/README.md new file mode 100644 index 0000000..997097f --- /dev/null +++ b/packages/angular/README.md @@ -0,0 +1,12 @@ +# @arxis/gtvzone-angular + +Bindings de Angular (≥ 16.1) para gtvzone: `provideGtvNavigation()`, `GtvNavigationService` +(signals y `state$`) y las directivas standalone `gtvFocusable`, `gtvZone` y `gtvLayer`. + +```html +
+
Play
+
+``` + +Documentación completa en el [README del repo](https://github.com/renearias/gtvzone#readme). diff --git a/packages/angular/ng-package.json b/packages/angular/ng-package.json new file mode 100644 index 0000000..8c363c4 --- /dev/null +++ b/packages/angular/ng-package.json @@ -0,0 +1,8 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "dist", + "lib": { + "entryFile": "src/public-api.ts" + }, + "allowedNonPeerDependencies": ["@arxis/gtvzone-core", "@arxis/gtvzone-dom"] +} diff --git a/packages/angular/package.json b/packages/angular/package.json new file mode 100644 index 0000000..ef085fa --- /dev/null +++ b/packages/angular/package.json @@ -0,0 +1,31 @@ +{ + "name": "@arxis/gtvzone-angular", + "version": "1.0.0-alpha.0", + "description": "Directivas y servicio de Angular para la navegación espacial de gtvzone", + "license": "MIT", + "sideEffects": false, + "keywords": ["angular", "smart-tv", "tizen", "webos", "spatial-navigation", "focus"], + "repository": { "type": "git", "url": "git+https://github.com/renearias/gtvzone.git", "directory": "packages/angular" }, + "scripts": { + "build": "ng-packagr -p ng-package.json -c tsconfig.lib.json", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "@arxis/gtvzone-core": "workspace:^", + "@arxis/gtvzone-dom": "workspace:^", + "tslib": "^2.8.0" + }, + "peerDependencies": { + "@angular/core": ">=16.1.0", + "rxjs": ">=7.0.0" + }, + "devDependencies": { + "@angular/common": "~22.1.0", + "@angular/compiler": "~22.1.0", + "@angular/compiler-cli": "~22.1.0", + "@angular/core": "~22.1.0", + "@angular/platform-browser": "~22.1.0", + "ng-packagr": "~22.1.0", + "rxjs": "^7.8.0" + } +} diff --git a/packages/angular/src/angular.test.ts b/packages/angular/src/angular.test.ts new file mode 100644 index 0000000..1d452a5 --- /dev/null +++ b/packages/angular/src/angular.test.ts @@ -0,0 +1,122 @@ +import { Component, provideZonelessChangeDetection, signal } from '@angular/core' +import { TestBed } from '@angular/core/testing' +import { afterEach, describe, expect, it } from 'vitest' +import { createNavigation } from '@arxis/gtvzone-core' +import type { Navigation, Rect } from '@arxis/gtvzone-core' +import { createDomAdapter } from '@arxis/gtvzone-dom' +import { GTV_NAVIGATION_DIRECTIVES, GtvNavigationService, provideGtvNavigation } from './public-api' + +const getRect = (element: Element): Rect => { + const [x, y, width, height] = (element.getAttribute('data-rect') ?? '0,0,0,0').split(',').map(Number) + return { x, y, width, height } +} + +const flush = () => new Promise(resolve => setTimeout(resolve, 0)) + +@Component({ + standalone: true, + imports: [...GTV_NAVIGATION_DIRECTIVES], + template: ` +
+
+
+
+
+
+
+ @if (modal()) { +
+
+ + +
+
+ } + ` +}) +class HostComponent { + readonly modal = signal(false) + readonly pressed = signal(null) +} + +async function setup () { + const nav = createNavigation({ adapter: createDomAdapter({ getRect }) }) + TestBed.configureTestingModule({ + providers: [provideZonelessChangeDetection(), provideGtvNavigation({ navigation: nav, keys: false })] + }) + const fixture = TestBed.createComponent(HostComponent) + await fixture.whenStable() + await flush() + return { fixture, nav, el: (id: string) => fixture.nativeElement.querySelector(`#${id}, [gtvFocusable="${id}"]`) as HTMLElement } +} + +const focusedIds = (root: HTMLElement) => + Array.from(root.querySelectorAll('.gtv-focused')).map(node => node.getAttribute('gtvFocusable')) + +afterEach(() => TestBed.resetTestingModule()) + +describe('Angular bindings', () => { + it('registers zones and elements and toggles the focused class', async () => { + const { fixture, nav } = await setup() + expect(nav.getFocusedId()).toBe('a1') + expect(focusedIds(fixture.nativeElement)).toEqual(['a1']) + nav.move('right') + expect(focusedIds(fixture.nativeElement)).toEqual(['a2']) + nav.move('down') + expect(nav.getFocusedId()).toBe('b1') + }) + + it('exposes focused() as a signal for templates', async () => { + const { fixture, nav } = await setup() + const a2 = () => fixture.nativeElement.querySelector('[gtvFocusable="a2"]').getAttribute('data-focused') + expect(a2()).toBe('false') + nav.move('right') + await fixture.whenStable() + expect(a2()).toBe('true') + }) + + it('emits gtvPress inside Angular', async () => { + const { fixture, nav } = await setup() + nav.press() + expect(fixture.componentInstance.pressed()).toBe('a1') + }) + + it('opens a layer, traps navigation in it and restores focus when it closes', async () => { + const { fixture, nav } = await setup() + nav.move('down') + fixture.componentInstance.modal.set(true) + await fixture.whenStable() + await flush() + expect(nav.getState()).toEqual({ focusedId: 'ok', activeLayer: 'modal' }) + nav.move('down') + expect(nav.getFocusedId()).toBe('cancel') + expect(nav.move('left')).toBe(false) + + fixture.componentInstance.modal.set(false) + await fixture.whenStable() + await flush() + expect(nav.getState()).toEqual({ focusedId: 'b1', activeLayer: 'default' }) + }) + + it('mirrors the state in the service signals', async () => { + const { nav } = await setup() + const service = TestBed.inject(GtvNavigationService) + expect(service.focusedId()).toBe('a1') + nav.move('right') + expect(service.focusedId()).toBe('a2') + expect(service.isFocused('a2')()).toBe(true) + }) +}) + +describe('provideGtvNavigation', () => { + it('creates a DOM-backed instance and binds keys by default', async () => { + TestBed.configureTestingModule({ providers: [provideZonelessChangeDetection(), provideGtvNavigation()] }) + const service = TestBed.inject(GtvNavigationService) + const nav: Navigation = service.navigation + const moves: string[] = [] + const original = nav.handleAction + nav.handleAction = action => { moves.push(action); return original(action) } + window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 40 })) + expect(moves).toEqual(['down']) + }) +}) diff --git a/packages/angular/src/directives.ts b/packages/angular/src/directives.ts new file mode 100644 index 0000000..c9b699e --- /dev/null +++ b/packages/angular/src/directives.ts @@ -0,0 +1,205 @@ +import { + booleanAttribute, + computed, + Directive, + ElementRef, + EventEmitter, + inject, + Input, + NgZone, + Output, + Renderer2, + signal +} from '@angular/core' +import type { OnChanges, OnDestroy, OnInit, Signal } from '@angular/core' +import type { BuiltInStrategy, DirectionMap, Strategy, ZoneOptions } from '@arxis/gtvzone-core' +import { GtvNavigationService } from './navigation.service' + +let idCounter = 0 +const nextId = (prefix: string) => `${prefix}-${++idCounter}` + +/** Isolates its content as a layer (e.g. a modal) while `gtvLayerActive` is true. */ +@Directive({ selector: '[gtvLayer]', standalone: true }) +export class GtvLayerDirective implements OnChanges, OnDestroy { + @Input({ required: true }) gtvLayer!: string + @Input({ transform: booleanAttribute }) gtvLayerActive = true + @Input() gtvLayerPriority?: number + + private readonly service = inject(GtvNavigationService) + private openedId: string | null = null + + ngOnChanges () { + this.close() + if (this.gtvLayerActive) { + this.service.navigation.openLayer(this.gtvLayer, { priority: this.gtvLayerPriority }) + this.openedId = this.gtvLayer + } + } + + ngOnDestroy () { + this.close() + } + + private close () { + if (this.openedId !== null) this.service.navigation.closeLayer(this.openedId) + this.openedId = null + } +} + +/** Groups focusable children into a zone. The closest `[gtvZone]` ancestor is the parent. */ +@Directive({ selector: '[gtvZone]', standalone: true, exportAs: 'gtvZone' }) +export class GtvZoneDirective implements OnInit, OnChanges, OnDestroy { + /** Zone id. Leave empty to generate one. */ + @Input() gtvZone = '' + @Input() gtvZoneStrategy?: BuiltInStrategy | Strategy + @Input() gtvZoneOrientation?: 'horizontal' | 'vertical' + @Input() gtvZoneColumns?: number + @Input({ transform: booleanAttribute }) gtvZoneWrap = false + @Input({ transform: booleanAttribute }) gtvZoneTrap = false + @Input({ transform: booleanAttribute }) gtvZoneDisabled = false + @Input() gtvZoneSaveLastFocused?: boolean + @Input() gtvZoneFirstFocus?: string + @Input() gtvZoneOrder?: number + @Input() gtvZoneNext?: DirectionMap + @Output() readonly gtvZoneEnter = new EventEmitter() + @Output() readonly gtvZoneLeave = new EventEmitter() + + private readonly service = inject(GtvNavigationService) + private readonly zone = inject(NgZone) + private readonly host = inject(ElementRef) + private readonly parent = inject(GtvZoneDirective, { optional: true, skipSelf: true }) + private readonly layer = inject(GtvLayerDirective, { optional: true }) + private readonly generatedId = nextId('gtvzone') + private registeredId: string | null = null + + get id (): string { + return this.gtvZone || this.generatedId + } + + ngOnInit () { + this.registeredId = this.id + this.service.navigation.registerZone({ + ...this.options(), + id: this.id, + parent: this.parent?.id, + layer: this.parent ? undefined : this.layer?.gtvLayer, + node: this.host.nativeElement, + onEnter: id => this.zone.run(() => this.gtvZoneEnter.emit(id)), + onLeave: id => this.zone.run(() => this.gtvZoneLeave.emit(id)) + }) + } + + ngOnChanges () { + if (this.registeredId !== null) this.service.navigation.updateZone(this.registeredId, this.options()) + } + + ngOnDestroy () { + if (this.registeredId !== null) this.service.navigation.unregisterZone(this.registeredId) + } + + private options (): Omit { + return { + strategy: this.gtvZoneStrategy, + orientation: this.gtvZoneOrientation, + columns: this.gtvZoneColumns, + wrap: this.gtvZoneWrap, + trap: this.gtvZoneTrap, + disabled: this.gtvZoneDisabled, + saveLastFocused: this.gtvZoneSaveLastFocused, + firstFocus: this.gtvZoneFirstFocus, + order: this.gtvZoneOrder, + nextZone: this.gtvZoneNext + } + } +} + +/** + * Registers the host element for remote-control navigation and toggles `gtvFocusedClass` + * on it. Use `#ref="gtvFocusable"` and `ref.focused()` for custom styling. + */ +@Directive({ selector: '[gtvFocusable]', standalone: true, exportAs: 'gtvFocusable' }) +export class GtvFocusableDirective implements OnInit, OnChanges, OnDestroy { + /** Element id. Leave empty to generate one. */ + @Input() gtvFocusable = '' + @Input({ transform: booleanAttribute }) gtvFocusableDisabled = false + @Input() gtvFocusableOrder?: number + @Input() gtvFocusableNext?: DirectionMap + /** Class added while focused. Set to `''` to style with `focused()` only. */ + @Input() gtvFocusedClass = 'gtv-focused' + @Output() readonly gtvFocus = new EventEmitter() + @Output() readonly gtvBlur = new EventEmitter() + @Output() readonly gtvPress = new EventEmitter() + + private readonly service = inject(GtvNavigationService) + private readonly zone = inject(NgZone) + private readonly renderer = inject(Renderer2) + private readonly host = inject(ElementRef) + private readonly parent = inject(GtvZoneDirective, { optional: true }) + private readonly layer = inject(GtvLayerDirective, { optional: true }) + private readonly generatedId = nextId('gtvzone-item') + private readonly registered = signal(null) + + /** Whether this element has the focus. */ + readonly focused: Signal = computed(() => { + const id = this.registered() + return id !== null && this.service.focusedId() === id + }) + + private get registeredId (): string | null { + return this.registered() + } + + get id (): string { + return this.gtvFocusable || this.generatedId + } + + ngOnInit () { + const id = this.id + this.registered.set(id) + this.service.navigation.register({ + id, + zone: this.parent?.id, + layer: this.parent ? undefined : this.layer?.gtvLayer, + node: this.host.nativeElement, + disabled: this.gtvFocusableDisabled, + order: this.gtvFocusableOrder, + nextFocus: this.gtvFocusableNext, + onFocus: () => { + this.toggleClass(true) + this.zone.run(() => this.gtvFocus.emit(id)) + }, + onBlur: () => { + this.toggleClass(false) + this.zone.run(() => this.gtvBlur.emit(id)) + }, + onPress: () => this.zone.run(() => this.gtvPress.emit(id)) + }) + } + + ngOnChanges () { + if (this.registeredId === null) return + this.service.navigation.update(this.registeredId, { + disabled: this.gtvFocusableDisabled, + order: this.gtvFocusableOrder, + nextFocus: this.gtvFocusableNext + }) + } + + ngOnDestroy () { + if (this.registeredId === null) return + this.toggleClass(false) + this.service.navigation.unregister(this.registeredId) + } + + focus (): boolean { + return this.service.setFocus(this.id) + } + + private toggleClass (on: boolean) { + if (!this.gtvFocusedClass) return + if (on) this.renderer.addClass(this.host.nativeElement, this.gtvFocusedClass) + else this.renderer.removeClass(this.host.nativeElement, this.gtvFocusedClass) + } +} + +export const GTV_NAVIGATION_DIRECTIVES = [GtvLayerDirective, GtvZoneDirective, GtvFocusableDirective] as const diff --git a/packages/angular/src/navigation.service.ts b/packages/angular/src/navigation.service.ts new file mode 100644 index 0000000..9a82fed --- /dev/null +++ b/packages/angular/src/navigation.service.ts @@ -0,0 +1,79 @@ +import { computed, DestroyRef, inject, Injectable, InjectionToken, makeEnvironmentProviders, NgZone, signal } from '@angular/core' +import type { EnvironmentProviders, Signal } from '@angular/core' +import { Observable } from 'rxjs' +import { createNavigation } from '@arxis/gtvzone-core' +import type { Direction, EventName, Navigation, NavigationEvents, NavigationState } from '@arxis/gtvzone-core' +import { bindKeys, createDomAdapter } from '@arxis/gtvzone-dom' +import type { BindKeysOptions, DomAdapterOptions } from '@arxis/gtvzone-dom' + +export const GTV_NAVIGATION = new InjectionToken('GTV_NAVIGATION') + +export interface GtvNavigationConfig { + /** Bring your own instance (e.g. shared with non-Angular code). Defaults to a DOM-backed one. */ + navigation?: Navigation | (() => Navigation) + adapter?: DomAdapterOptions + autoFocus?: boolean + /** Key binding options, or `false` to wire input yourself. */ + keys?: BindKeysOptions | false +} + +/** + * Registers the navigation instance. Keys are listened to outside the Angular zone, so a + * key press only triggers change detection when focus actually changes. + */ +export function provideGtvNavigation (config: GtvNavigationConfig = {}): EnvironmentProviders { + return makeEnvironmentProviders([{ + provide: GTV_NAVIGATION, + useFactory: () => { + const { navigation, adapter, autoFocus, keys } = config + const nav = typeof navigation === 'function' + ? navigation() + : navigation ?? createNavigation({ adapter: createDomAdapter(adapter), autoFocus }) + if (keys !== false) { + const unbind = inject(NgZone).runOutsideAngular(() => bindKeys(nav, keys)) + inject(DestroyRef).onDestroy(unbind) + } + return nav + } + }]) +} + +@Injectable({ providedIn: 'root' }) +export class GtvNavigationService { + readonly navigation = inject(GTV_NAVIGATION) + private readonly zone = inject(NgZone) + private readonly stateSignal = signal(this.navigation.getState()) + + readonly state: Signal = this.stateSignal.asReadonly() + readonly focusedId = computed(() => this.stateSignal().focusedId) + readonly activeLayer = computed(() => this.stateSignal().activeLayer) + + /** Same state as an Observable, for RxJS-based code. */ + readonly state$ = new Observable(subscriber => { + subscriber.next(this.navigation.getState()) + return this.navigation.subscribe(() => subscriber.next(this.navigation.getState())) + }) + + constructor () { + const unsubscribe = this.navigation.subscribe(() => { + this.zone.run(() => this.stateSignal.set(this.navigation.getState())) + }) + inject(DestroyRef).onDestroy(unsubscribe) + } + + isFocused (id: string): Signal { + return computed(() => this.stateSignal().focusedId === id) + } + + /** Navigation events as an Observable; handlers run inside the Angular zone. */ + on (event: E): Observable { + return new Observable(subscriber => + this.navigation.on(event, payload => this.zone.run(() => subscriber.next(payload))) + ) + } + + move (direction: Direction) { return this.navigation.move(direction) } + setFocus (id: string) { return this.navigation.setFocus(id) } + press () { return this.navigation.press() } + back () { this.navigation.back() } +} diff --git a/packages/angular/src/public-api.ts b/packages/angular/src/public-api.ts new file mode 100644 index 0000000..3b5e156 --- /dev/null +++ b/packages/angular/src/public-api.ts @@ -0,0 +1,3 @@ +export { GTV_NAVIGATION, GtvNavigationService, provideGtvNavigation } from './navigation.service' +export type { GtvNavigationConfig } from './navigation.service' +export { GTV_NAVIGATION_DIRECTIVES, GtvFocusableDirective, GtvLayerDirective, GtvZoneDirective } from './directives' diff --git a/packages/angular/src/test-setup.ts b/packages/angular/src/test-setup.ts new file mode 100644 index 0000000..59e432b --- /dev/null +++ b/packages/angular/src/test-setup.ts @@ -0,0 +1,5 @@ +import '@angular/compiler' +import { TestBed } from '@angular/core/testing' +import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing' + +TestBed.initTestEnvironment(BrowserTestingModule, platformBrowserTesting()) diff --git a/packages/angular/tsconfig.json b/packages/angular/tsconfig.json new file mode 100644 index 0000000..556139f --- /dev/null +++ b/packages/angular/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "experimentalDecorators": true, + "useDefineForClassFields": false + }, + "include": ["src"] +} diff --git a/packages/angular/tsconfig.lib.json b/packages/angular/tsconfig.lib.json new file mode 100644 index 0000000..2aa500d --- /dev/null +++ b/packages/angular/tsconfig.lib.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declarationMap": false, + "inlineSources": true, + "sourceMap": true, + "types": [] + }, + "angularCompilerOptions": { + "compilationMode": "partial", + "strictTemplates": true, + "strictInjectionParameters": true + }, + "exclude": ["src/**/*.test.ts", "src/test-setup.ts"] +} diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 0000000..cb8c99b --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,18 @@ +# @arxis/gtvzone-core + +Motor de navegación espacial para Smart TV: registro de elementos, zonas con estrategias +(`geometry`, `list`, `grid` o una propia), capas con prioridad y un store con `subscribe`. +No conoce el DOM ni ningún framework: la geometría llega por un adapter. + +```js +import { createNavigation } from '@arxis/gtvzone-core' + +const nav = createNavigation({ adapter: { measure: (id, node) => rectOf(node) } }) +nav.registerZone({ id: 'row', strategy: 'list' }) +nav.register({ id: 'a', zone: 'row', onPress: play }) +nav.move('right') +nav.subscribe(() => render(nav.getState().focusedId)) +``` + +En el navegador usá [`@arxis/gtvzone-dom`](../dom). Documentación completa en el +[README del repo](https://github.com/renearias/gtvzone#readme). diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..d0dd24b --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,25 @@ +{ + "name": "@arxis/gtvzone-core", + "version": "1.0.0-alpha.0", + "description": "Motor de navegación espacial para Smart TV, sin DOM ni framework", + "license": "MIT", + "type": "module", + "sideEffects": false, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": ["dist"], + "keywords": ["smart-tv", "tizen", "webos", "spatial-navigation", "focus", "remote-control"], + "repository": { "type": "git", "url": "git+https://github.com/renearias/gtvzone.git", "directory": "packages/core" }, + "scripts": { + "build": "tsup", + "typecheck": "tsc --noEmit -p tsconfig.json" + } +} diff --git a/packages/core/src/geometry.ts b/packages/core/src/geometry.ts new file mode 100644 index 0000000..efc14c6 --- /dev/null +++ b/packages/core/src/geometry.ts @@ -0,0 +1,67 @@ +import type { Direction, Rect } from './types' + +const ORTHOGONAL_WEIGHT = 2 + +const isHorizontal = (direction: Direction) => direction === 'left' || direction === 'right' + +const center = (rect: Rect) => ({ x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 }) + +/** Gap between two 1-D ranges, 0 when they overlap. */ +const rangeGap = (startA: number, endA: number, startB: number, endB: number) => + Math.max(0, startB - endA, startA - endB) + +export function unionRects (rects: Rect[]): Rect | null { + if (rects.length === 0) return null + let left = Infinity + let top = Infinity + let right = -Infinity + let bottom = -Infinity + for (const rect of rects) { + left = Math.min(left, rect.x) + top = Math.min(top, rect.y) + right = Math.max(right, rect.x + rect.width) + bottom = Math.max(bottom, rect.y + rect.height) + } + return { x: left, y: top, width: right - left, height: bottom - top } +} + +/** True when `candidate` lies in `direction` from `origin` (judged by centers). */ +export function isInDirection (origin: Rect, candidate: Rect, direction: Direction): boolean { + const from = center(origin) + const to = center(candidate) + switch (direction) { + case 'right': return to.x > from.x + case 'left': return to.x < from.x + case 'down': return to.y > from.y + case 'up': return to.y < from.y + } +} + +/** + * Lower is better. Distance along the movement axis plus a weighted penalty for being out of + * line on the other axis, so a slightly farther item in the same row beats a closer one in + * another row. + */ +export function directionalScore (origin: Rect, candidate: Rect, direction: Direction): number { + let primary: number + let orthogonal: number + if (isHorizontal(direction)) { + primary = direction === 'right' + ? candidate.x - (origin.x + origin.width) + : origin.x - (candidate.x + candidate.width) + orthogonal = rangeGap(origin.y, origin.y + origin.height, candidate.y, candidate.y + candidate.height) + } else { + primary = direction === 'down' + ? candidate.y - (origin.y + origin.height) + : origin.y - (candidate.y + candidate.height) + orthogonal = rangeGap(origin.x, origin.x + origin.width, candidate.x, candidate.x + candidate.width) + } + return Math.max(0, primary) + orthogonal * ORTHOGONAL_WEIGHT +} + +/** Distance between centers on the axis orthogonal to `direction`, used to break ties. */ +export function alignment (origin: Rect, candidate: Rect, direction: Direction): number { + const from = center(origin) + const to = center(candidate) + return isHorizontal(direction) ? Math.abs(to.y - from.y) : Math.abs(to.x - from.x) +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 0000000..7d0dcd6 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,5 @@ +export { createNavigation, DEFAULT_LAYER } from './navigation' +export type { Navigation } from './navigation' +export { geometryStrategy, listStrategy, gridStrategy, resolveStrategy } from './strategies' +export { directionalScore, isInDirection, unionRects } from './geometry' +export type * from './types' diff --git a/packages/core/src/navigation.test.ts b/packages/core/src/navigation.test.ts new file mode 100644 index 0000000..26b7087 --- /dev/null +++ b/packages/core/src/navigation.test.ts @@ -0,0 +1,280 @@ +import { describe, expect, it, vi } from 'vitest' +import { createNavigation } from './navigation' +import type { NavigationAdapter, Rect } from './types' + +type Box = [x: number, y: number, width: number, height: number] + +/** Fake adapter: rects come from a table and deferred work runs when the test calls `flush`. */ +function setup (boxes: Record = {}, options: { autoFocus?: boolean } = {}) { + const queue: Array<() => void> = [] + const measure = vi.fn((id: string): Rect | null => { + const box = boxes[id] + return box ? { x: box[0], y: box[1], width: box[2], height: box[3] } : null + }) + const adapter: NavigationAdapter = { measure, schedule: task => { queue.push(task) } } + const nav = createNavigation({ adapter, ...options }) + const flush = () => { while (queue.length) queue.shift()!() } + return { nav, flush, measure } +} + +describe('geometry navigation', () => { + const grid: Record = { + a: [0, 0, 100, 100], b: [120, 0, 100, 100], + c: [0, 120, 100, 100], d: [120, 120, 100, 100] + } + + it('moves to the closest element in each direction', () => { + const { nav, flush } = setup(grid) + Object.keys(grid).forEach(id => nav.register({ id })) + flush() + expect(nav.getFocusedId()).toBe('a') + expect(nav.move('right')).toBe(true) + expect(nav.getFocusedId()).toBe('b') + nav.move('down') + expect(nav.getFocusedId()).toBe('d') + nav.move('left') + expect(nav.getFocusedId()).toBe('c') + nav.move('up') + expect(nav.getFocusedId()).toBe('a') + }) + + it('emits boundary and keeps focus when there is nowhere to go', () => { + const { nav, flush } = setup(grid) + Object.keys(grid).forEach(id => nav.register({ id })) + flush() + const onBoundary = vi.fn() + nav.on('boundary', onBoundary) + expect(nav.move('left')).toBe(false) + expect(nav.getFocusedId()).toBe('a') + expect(onBoundary).toHaveBeenCalledWith({ direction: 'left', focusedId: 'a' }) + }) + + it('prefers an aligned element over a closer one in another row', () => { + const { nav, flush } = setup({ + origin: [0, 0, 100, 100], + aligned: [400, 0, 100, 100], + offRow: [150, 300, 100, 100] + }) + ;['origin', 'aligned', 'offRow'].forEach(id => nav.register({ id })) + flush() + nav.move('right') + expect(nav.getFocusedId()).toBe('aligned') + }) + + it('does not auto focus when autoFocus is false', () => { + const { nav, flush } = setup(grid, { autoFocus: false }) + nav.register({ id: 'a' }) + flush() + expect(nav.getFocusedId()).toBeNull() + expect(nav.setFocus('a')).toBe(true) + }) +}) + +describe('zones', () => { + function rows () { + const env = setup({ + r1a: [0, 0, 100, 100], r1b: [120, 0, 100, 100], r1c: [240, 0, 100, 100], + r2a: [0, 200, 100, 100], r2b: [120, 200, 100, 100], r2c: [240, 200, 100, 100] + }) + env.nav.registerZone({ id: 'row1', strategy: 'list' }) + env.nav.registerZone({ id: 'row2', strategy: 'list' }) + ;['r1a', 'r1b', 'r1c'].forEach(id => env.nav.register({ id, zone: 'row1' })) + ;['r2a', 'r2b', 'r2c'].forEach(id => env.nav.register({ id, zone: 'row2' })) + env.flush() + return env + } + + it('moves inside a list zone without measuring', () => { + const { nav, measure } = rows() + measure.mockClear() + nav.move('right') + nav.move('right') + expect(nav.getFocusedId()).toBe('r1c') + expect(measure).not.toHaveBeenCalled() + }) + + it('enters the next zone at the closest element and remembers the last one', () => { + const { nav } = rows() + nav.move('right') + nav.move('right') + nav.move('down') + expect(nav.getFocusedId()).toBe('r2c') + nav.move('left') + nav.move('up') + expect(nav.getFocusedId()).toBe('r1c') + nav.move('down') + expect(nav.getFocusedId()).toBe('r2b') + }) + + it('wraps inside a list zone', () => { + const { nav } = rows() + nav.updateZone('row1', { wrap: true }) + nav.move('left') + expect(nav.getFocusedId()).toBe('r1c') + }) + + it('keeps focus inside a trapped zone', () => { + const { nav } = rows() + nav.updateZone('row1', { trap: true }) + expect(nav.move('down')).toBe(false) + expect(nav.getFocusedId()).toBe('r1a') + }) + + it('follows nextZone and nextFocus overrides', () => { + const { nav } = rows() + nav.updateZone('row1', { nextZone: { down: null } }) + expect(nav.move('down')).toBe(false) + nav.update('r1a', { nextFocus: { right: 'r2c' } }) + nav.move('right') + expect(nav.getFocusedId()).toBe('r2c') + }) + + it('skips disabled elements and empty zones', () => { + const { nav } = rows() + nav.update('r1b', { disabled: true }) + nav.move('right') + expect(nav.getFocusedId()).toBe('r1c') + nav.updateZone('row2', { disabled: true }) + expect(nav.move('down')).toBe(false) + }) + + it('enters a zone through setFocus using firstFocus', () => { + const { nav } = rows() + nav.updateZone('row2', { firstFocus: 'r2b' }) + expect(nav.setFocus('row2')).toBe(true) + expect(nav.getFocusedId()).toBe('r2b') + }) + + it('fires zone callbacks and events in order', () => { + const { nav } = rows() + const log: string[] = [] + nav.updateZone('row1', { onLeave: id => log.push('leave:' + id) }) + nav.updateZone('row2', { onEnter: id => log.push('enter:' + id) }) + nav.update('r1a', { onBlur: id => log.push('blur:' + id) }) + nav.update('r2a', { onFocus: id => log.push('focus:' + id) }) + nav.move('down') + expect(log).toEqual(['blur:r1a', 'leave:row1', 'enter:row2', 'focus:r2a']) + }) + + it('refocuses inside the same zone when the focused element is removed', () => { + const { nav, flush } = rows() + nav.move('down') + nav.move('right') + nav.unregister('r2b') + flush() + expect(nav.getFocusedId()).toBe('r2a') + }) +}) + +describe('grid strategy', () => { + it('moves by rows and columns and lands on the short last row', () => { + const { nav, flush } = setup() + nav.registerZone({ id: 'grid', strategy: 'grid', columns: 3 }) + for (let i = 0; i < 7; i++) nav.register({ id: 'i' + i, zone: 'grid' }) + flush() + nav.move('down') + expect(nav.getFocusedId()).toBe('i3') + nav.move('right') + expect(nav.getFocusedId()).toBe('i4') + nav.move('down') + expect(nav.getFocusedId()).toBe('i6') + nav.move('up') + expect(nav.getFocusedId()).toBe('i3') + }) +}) + +describe('layers', () => { + it('isolates the top layer and restores focus when it closes', () => { + const { nav, flush } = setup({ a: [0, 0, 10, 10], b: [20, 0, 10, 10], m1: [0, 0, 10, 10], m2: [0, 20, 10, 10] }) + nav.register({ id: 'a' }) + nav.register({ id: 'b' }) + flush() + nav.move('right') + expect(nav.getFocusedId()).toBe('b') + + nav.register({ id: 'm1', layer: 'modal' }) + nav.register({ id: 'm2', layer: 'modal' }) + nav.openLayer('modal') + expect(nav.getState()).toEqual({ focusedId: 'm1', activeLayer: 'modal' }) + nav.move('down') + expect(nav.getFocusedId()).toBe('m2') + expect(nav.move('right')).toBe(false) + expect(nav.setFocus('a')).toBe(false) + + nav.closeLayer('modal') + expect(nav.getState()).toEqual({ focusedId: 'b', activeLayer: 'default' }) + }) + + it('clears focus while a layer has nothing focusable yet, then focuses its first element', () => { + const { nav, flush } = setup({ a: [0, 0, 10, 10], m1: [0, 0, 10, 10] }) + nav.register({ id: 'a' }) + flush() + nav.openLayer('modal') + expect(nav.getFocusedId()).toBeNull() + nav.register({ id: 'm1', layer: 'modal' }) + flush() + expect(nav.getFocusedId()).toBe('m1') + }) +}) + +describe('store contract', () => { + it('notifies subscribers only on change and keeps the snapshot stable', () => { + const { nav, flush } = setup({ a: [0, 0, 10, 10], b: [20, 0, 10, 10] }) + nav.register({ id: 'a' }) + nav.register({ id: 'b' }) + flush() + const listener = vi.fn() + const unsubscribe = nav.subscribe(listener) + const snapshot = nav.getState() + nav.move('left') + expect(listener).not.toHaveBeenCalled() + expect(nav.getState()).toBe(snapshot) + nav.move('right') + expect(listener).toHaveBeenCalledTimes(1) + unsubscribe() + nav.move('left') + expect(listener).toHaveBeenCalledTimes(1) + }) + + it('dispatches enter to onPress and back to the back event', () => { + const { nav, flush } = setup() + const onPress = vi.fn() + const onBack = vi.fn() + nav.register({ id: 'a', onPress }) + nav.on('back', onBack) + flush() + expect(nav.handleAction('enter')).toBe(true) + expect(onPress).toHaveBeenCalledWith('a') + nav.handleAction('back') + expect(onBack).toHaveBeenCalledWith({ focusedId: 'a' }) + }) + + it('keeps registration order stable when an element re-registers (React StrictMode)', () => { + const { nav, flush } = setup() + nav.registerZone({ id: 'row', strategy: 'list' }) + ;['a', 'b', 'c'].forEach(id => nav.register({ id, zone: 'row' })) + nav.unregister('a') + nav.register({ id: 'a', zone: 'row' }) + flush() + expect(nav.getFocusedId()).toBe('a') + nav.move('right') + expect(nav.getFocusedId()).toBe('b') + }) + + it('orders siblings with adapter.compare when there is no explicit order', () => { + const position: Record = { a: 2, b: 1 } + const queue: Array<() => void> = [] + const nav = createNavigation({ + adapter: { + measure: () => null, + compare: (x, y) => position[x.id] - position[y.id], + schedule: task => { queue.push(task) } + } + }) + nav.registerZone({ id: 'row', strategy: 'list' }) + nav.register({ id: 'a', zone: 'row' }) + nav.register({ id: 'b', zone: 'row' }) + queue.shift()!() + expect(nav.getFocusedId()).toBe('b') + }) +}) diff --git a/packages/core/src/navigation.ts b/packages/core/src/navigation.ts new file mode 100644 index 0000000..5a67265 --- /dev/null +++ b/packages/core/src/navigation.ts @@ -0,0 +1,489 @@ +import { unionRects } from './geometry' +import { resolveStrategy } from './strategies' +import type { + Action, + Direction, + EventName, + FocusableOptions, + LayerOptions, + NavigationAdapter, + NavigationEvents, + NavigationOptions, + NavigationState, + Rect, + StrategyContext, + Unit, + ZoneOptions +} from './types' + +export const DEFAULT_LAYER = 'default' +const ROOT_PREFIX = '@root:' + +const rootId = (layer: string) => ROOT_PREFIX + layer +const isRoot = (zoneId: string) => zoneId.indexOf(ROOT_PREFIX) === 0 +const layerOfRoot = (zoneId: string) => zoneId.slice(ROOT_PREFIX.length) + +interface ElementRecord extends FocusableOptions { seq: number } +interface ZoneRecord extends ZoneOptions { seq: number, lastFocused: string | null } +interface LayerRecord { id: string, priority: number, seq: number } + +type Listener = () => void +type EventHandler = (payload: NavigationEvents[E]) => void + +export interface Navigation { + register(options: FocusableOptions): () => void + unregister(id: string): void + update(id: string, patch: Partial>): void + registerZone(options: ZoneOptions): () => void + unregisterZone(id: string): void + updateZone(id: string, patch: Partial>): void + openLayer(id: string, options?: LayerOptions): void + closeLayer(id: string): void + /** Moves focus in `direction`. Returns `false` when there was nowhere to go. */ + move(direction: Direction): boolean + /** Triggers `onPress` of the focused element. */ + press(): boolean + back(): void + /** Dispatches an abstract input action. Returns `true` when it was handled. */ + handleAction(action: Action): boolean + /** Focuses an element, or enters a zone. Returns `false` if the target is not reachable. */ + setFocus(id: string): boolean + getState(): NavigationState + getFocusedId(): string | null + isFocused(id: string): boolean + subscribe(listener: Listener): () => void + on(event: E, handler: EventHandler): () => void + destroy(): void +} + +const defaultSchedule = (task: () => void) => { + // queueMicrotask is missing on older TV browsers (Chromium < 71). + Promise.resolve().then(task) +} + +const nullAdapter: NavigationAdapter = { measure: () => null } + +export function createNavigation (options: NavigationOptions = {}): Navigation { + const adapter = options.adapter ?? nullAdapter + const schedule = adapter.schedule ?? defaultSchedule + const autoFocus = options.autoFocus !== false + + const elements = new Map() + const zones = new Map() + const roots = new Map() + const layers = new Map([[DEFAULT_LAYER, { id: DEFAULT_LAYER, priority: 0, seq: 0 }]]) + const savedFocus = new Map() + const listeners = new Set() + const handlers = new Map>>() + + let seq = 0 + // Survives unregister so a re-mount (e.g. React StrictMode) keeps its place in the order. + const seqById = new Map() + const seqFor = (key: string) => { + let value = seqById.get(key) + if (value === undefined) seqById.set(key, value = ++seq) + return value + } + let state: NavigationState = { focusedId: null, activeLayer: DEFAULT_LAYER } + let focusedChain: string[] = [] + let lostZone: string | null = null + let repairPending = false + let rectCache: Map | null = null + + // ---------------------------------------------------------------- state & events + + function setState (patch: Partial) { + state = { ...state, ...patch } + listeners.forEach(listener => listener()) + } + + function emit (event: E, payload: NavigationEvents[E]) { + handlers.get(event)?.forEach(handler => handler(payload)) + } + + // ---------------------------------------------------------------- tree resolution + + const elementZone = (el: FocusableOptions) => el.zone ?? rootId(el.layer ?? DEFAULT_LAYER) + const zoneParent = (zone: ZoneOptions) => zone.parent ?? rootId(zone.layer ?? DEFAULT_LAYER) + + function getZone (zoneId: string): ZoneRecord | undefined { + if (!isRoot(zoneId)) return zones.get(zoneId) + let root = roots.get(zoneId) + if (!root) { + root = { id: zoneId, strategy: 'geometry', seq: 0, lastFocused: null } + roots.set(zoneId, root) + } + return root + } + + /** Zone ids from `zoneId` up to its layer root, or `null` if a link is missing or disabled. */ + function chainOf (zoneId: string): string[] | null { + const chain: string[] = [] + let current = zoneId + while (!isRoot(current)) { + const zone = zones.get(current) + if (!zone || zone.disabled || chain.indexOf(current) !== -1) return null + chain.push(current) + current = zoneParent(zone) + } + chain.push(current) + return chain + } + + const inActiveLayer = (chain: string[] | null): chain is string[] => + !!chain && layerOfRoot(chain[chain.length - 1]) === state.activeLayer + + function isReachableElement (id: string | null): id is string { + if (id === null) return false + const el = elements.get(id) + return !!el && !el.disabled && inActiveLayer(chainOf(elementZone(el))) + } + + const isReachableZone = (id: string) => zones.has(id) && inActiveLayer(chainOf(id)) + + function compareUnits (a: ElementRecord | ZoneRecord, b: ElementRecord | ZoneRecord): number { + if (a.order !== undefined && b.order !== undefined) return a.order - b.order + const byAdapter = adapter.compare?.({ id: a.id, node: a.node }, { id: b.id, node: b.node }) ?? 0 + return byAdapter || a.seq - b.seq + } + + /** Direct, enabled children of a zone, in navigation order. */ + function childrenOf (zoneId: string): Unit[] { + const records: Array<{ kind: Unit['kind'], record: ElementRecord | ZoneRecord }> = [] + elements.forEach(el => { if (!el.disabled && elementZone(el) === zoneId) records.push({ kind: 'element', record: el }) }) + zones.forEach(zone => { if (!zone.disabled && zoneParent(zone) === zoneId) records.push({ kind: 'zone', record: zone }) }) + return records + .sort((a, b) => compareUnits(a.record, b.record)) + .map((item, index) => ({ kind: item.kind, id: item.record.id, order: index })) + } + + // ---------------------------------------------------------------- measuring + + function rectOf (unit: Unit): Rect | null { + const key = unit.kind + ':' + unit.id + if (rectCache?.has(key)) return rectCache.get(key)! + let rect: Rect | null + if (unit.kind === 'element') { + rect = adapter.measure(unit.id, elements.get(unit.id)?.node) + } else { + rect = adapter.measure(unit.id, zones.get(unit.id)?.node) ?? + unionRects(childrenOf(unit.id).map(rectOf).filter((r): r is Rect => r !== null)) + } + rectCache?.set(key, rect) + return rect + } + + /** Runs `fn` with a per-operation rect cache so each node is measured at most once. */ + function measuring (fn: () => T): T { + if (rectCache) return fn() + rectCache = new Map() + try { + return fn() + } finally { + rectCache = null + } + } + + const contextFor = (zone: ZoneRecord): StrategyContext => ({ zone, rect: rectOf }) + + // ---------------------------------------------------------------- resolving targets + + /** `originRect` is lazy so moving between plain elements never measures the layout. */ + function resolveUnit (unit: Unit, direction: Direction | null, originRect: () => Rect | null): string | null { + return unit.kind === 'element' ? unit.id : enterZone(unit.id, direction, originRect()) + } + + /** Finds the element that should receive focus when entering `zoneId`. */ + function enterZone (zoneId: string, direction: Direction | null, originRect: Rect | null): string | null { + const zone = getZone(zoneId) + if (!zone) return null + const units = childrenOf(zoneId) + if (units.length === 0) return null + + const preferred: Unit[] = [] + const pick = (id: string | null | undefined) => { + const unit = id ? units.find(u => u.id === id) : undefined + if (unit) preferred.push(unit) + } + if (zone.saveLastFocused !== false) pick(zone.lastFocused) + pick(zone.firstFocus) + + const candidates = preferred.concat(resolveStrategy(zone.strategy).enter(units, direction, originRect, contextFor(zone))) + const tried = new Set() + for (const candidate of candidates) { + if (tried.has(candidate.id)) continue + tried.add(candidate.id) + const target = resolveUnit(candidate, direction, () => originRect) + if (target) return target + } + return null + } + + function resolveTarget (id: string, direction: Direction | null, originRect: Rect | null): string | null { + if (isReachableElement(id)) return id + if (isReachableZone(id)) return enterZone(id, direction, originRect) + return null + } + + // ---------------------------------------------------------------- focusing + + function focusElement (id: string) { + const previousId = state.focusedId + if (previousId === id) return + const el = elements.get(id)! + const chain = chainOf(elementZone(el)) ?? [] + + // Remember, for every ancestor, which direct child leads to the new focus. + let child = id + for (const zoneId of chain) { + getZone(zoneId)!.lastFocused = child + child = zoneId + } + + const userChain = chain.filter(zoneId => !isRoot(zoneId)) + const left = focusedChain.filter(zoneId => userChain.indexOf(zoneId) === -1) + const entered = userChain.filter(zoneId => focusedChain.indexOf(zoneId) === -1).reverse() + + if (previousId !== null) { + elements.get(previousId)?.onBlur?.(previousId) + emit('blur', { id: previousId, nextId: id }) + } + left.forEach(zoneId => { + zones.get(zoneId)?.onLeave?.(zoneId) + emit('leaveZone', { id: zoneId }) + }) + + focusedChain = userChain + lostZone = null + setState({ focusedId: id }) + + entered.forEach(zoneId => { + zones.get(zoneId)?.onEnter?.(zoneId) + emit('enterZone', { id: zoneId }) + }) + el.onFocus?.(id) + emit('focus', { id, previousId }) + } + + function clearFocus () { + const previousId = state.focusedId + if (previousId === null) return + elements.get(previousId)?.onBlur?.(previousId) + emit('blur', { id: previousId, nextId: null }) + focusedChain.forEach(zoneId => { + zones.get(zoneId)?.onLeave?.(zoneId) + emit('leaveZone', { id: zoneId }) + }) + focusedChain = [] + setState({ focusedId: null }) + } + + /** Makes sure focus sits on a reachable element, moving or clearing it otherwise. */ + function repair () { + if (isReachableElement(state.focusedId)) return + measuring(() => { + const fromLostZone = lostZone && isReachableZone(lostZone) ? enterZone(lostZone, null, null) : null + const target = fromLostZone ?? (autoFocus ? enterZone(rootId(state.activeLayer), null, null) : null) + if (target) focusElement(target) + else clearFocus() + }) + } + + function scheduleRepair () { + if (repairPending) return + repairPending = true + schedule(() => { + repairPending = false + repair() + }) + } + + // ---------------------------------------------------------------- movement + + function move (direction: Direction): boolean { + return measuring(() => { + const focusedId = state.focusedId + if (!isReachableElement(focusedId)) { + repair() + return state.focusedId !== null + } + const el = elements.get(focusedId)! + const elementUnit: Unit = { kind: 'element', id: focusedId, order: 0 } + const originRect = () => rectOf(elementUnit) + + const jump = (target: string | null): boolean => { + const id = target === null ? null : resolveTarget(target, direction, originRect()) + if (id) focusElement(id) + return !!id + } + + const override = el.nextFocus?.[direction] + if (override !== undefined) return jump(override) || boundary(direction) + + let origin = elementUnit + let zoneId = elementZone(el) + while (true) { + const zone = getZone(zoneId)! + const ranked = resolveStrategy(zone.strategy).rank(origin, childrenOf(zoneId), direction, contextFor(zone)) + for (const candidate of ranked) { + const target = resolveUnit(candidate, direction, originRect) + if (target) { + focusElement(target) + return true + } + } + if (isRoot(zoneId) || zone.trap) return boundary(direction) + const exit = zone.nextZone?.[direction] + if (exit !== undefined) return jump(exit) || boundary(direction) + origin = { kind: 'zone', id: zoneId, order: 0 } + zoneId = zoneParent(zone) + } + }) + } + + function boundary (direction: Direction): false { + emit('boundary', { direction, focusedId: state.focusedId }) + return false + } + + // ---------------------------------------------------------------- layers + + function computeActiveLayer (): string { + let active: LayerRecord | null = null + layers.forEach(layer => { + if (!active || layer.priority > active.priority || (layer.priority === active.priority && layer.seq > active.seq)) { + active = layer + } + }) + return (active as LayerRecord | null)?.id ?? DEFAULT_LAYER + } + + function refreshActiveLayer () { + const previousLayer = state.activeLayer + const activeLayer = computeActiveLayer() + if (activeLayer === previousLayer) return + if (state.focusedId !== null) savedFocus.set(previousLayer, state.focusedId) + setState({ activeLayer }) + emit('layerChange', { activeLayer, previousLayer }) + + const restore = savedFocus.get(activeLayer) + savedFocus.delete(activeLayer) + if (restore && isReachableElement(restore)) focusElement(restore) + else repair() + } + + // ---------------------------------------------------------------- public API + + const api: Navigation = { + register (focusable) { + elements.set(focusable.id, { ...focusable, seq: seqFor('element:' + focusable.id) }) + scheduleRepair() + return () => api.unregister(focusable.id) + }, + + unregister (id) { + const el = elements.get(id) + if (!el) return + if (state.focusedId === id) lostZone = elementZone(el) + elements.delete(id) + scheduleRepair() + }, + + update (id, patch) { + const el = elements.get(id) + if (!el) return + elements.set(id, { ...el, ...patch }) + scheduleRepair() + }, + + registerZone (zone) { + const previous = zones.get(zone.id) + zones.set(zone.id, { ...zone, seq: seqFor('zone:' + zone.id), lastFocused: previous?.lastFocused ?? null }) + scheduleRepair() + return () => api.unregisterZone(zone.id) + }, + + unregisterZone (id) { + if (!zones.delete(id)) return + focusedChain = focusedChain.filter(zoneId => zoneId !== id) + scheduleRepair() + }, + + updateZone (id, patch) { + const zone = zones.get(id) + if (!zone) return + zones.set(id, { ...zone, ...patch }) + scheduleRepair() + }, + + openLayer (id, layerOptions = {}) { + const previous = layers.get(id) + layers.set(id, { id, priority: layerOptions.priority ?? previous?.priority ?? 1, seq: previous?.seq ?? ++seq }) + refreshActiveLayer() + }, + + closeLayer (id) { + if (id === DEFAULT_LAYER || !layers.delete(id)) return + savedFocus.delete(id) + refreshActiveLayer() + }, + + move, + + press () { + const id = state.focusedId + if (!isReachableElement(id)) return false + elements.get(id)!.onPress?.(id) + emit('press', { id }) + return true + }, + + back () { + emit('back', { focusedId: state.focusedId }) + }, + + handleAction (action) { + switch (action) { + case 'enter': return api.press() + case 'back': api.back(); return true + default: return api.move(action) + } + }, + + setFocus (id) { + const target = measuring(() => resolveTarget(id, null, null)) + if (target) focusElement(target) + return !!target + }, + + getState: () => state, + getFocusedId: () => state.focusedId, + isFocused: id => state.focusedId === id, + + subscribe (listener) { + listeners.add(listener) + return () => { listeners.delete(listener) } + }, + + on (event, handler) { + let set = handlers.get(event) + if (!set) handlers.set(event, set = new Set()) + set.add(handler) + return () => { set!.delete(handler) } + }, + + destroy () { + elements.clear() + zones.clear() + roots.clear() + listeners.clear() + handlers.clear() + savedFocus.clear() + seqById.clear() + focusedChain = [] + state = { focusedId: null, activeLayer: DEFAULT_LAYER } + } + } + + return api +} diff --git a/packages/core/src/strategies.ts b/packages/core/src/strategies.ts new file mode 100644 index 0000000..62c5db9 --- /dev/null +++ b/packages/core/src/strategies.ts @@ -0,0 +1,121 @@ +import { alignment, directionalScore, isInDirection } from './geometry' +import type { BuiltInStrategy, Direction, Rect, Strategy, StrategyContext, Unit } from './types' + +const byOrder = (a: Unit, b: Unit) => a.order - b.order + +interface Measured { unit: Unit, rect: Rect } + +function measureAll (units: Unit[], ctx: StrategyContext): { measured: Measured[], unmeasured: Unit[] } { + const measured: Measured[] = [] + const unmeasured: Unit[] = [] + for (const unit of units) { + const rect = ctx.rect(unit) + if (rect) measured.push({ unit, rect }) + else unmeasured.push(unit) + } + return { measured, unmeasured } +} + +function sortByDirection (origin: Rect, items: Measured[], direction: Direction): Unit[] { + return items + .map(item => ({ + unit: item.unit, + score: directionalScore(origin, item.rect, direction), + tie: alignment(origin, item.rect, direction) + })) + .sort((a, b) => a.score - b.score || a.tie - b.tie) + .map(item => item.unit) +} + +const readingOrder = (a: Measured, b: Measured) => a.rect.y - b.rect.y || a.rect.x - b.rect.x + +/** Picks the closest unit in the pressed direction using measured rects. */ +export const geometryStrategy: Strategy = { + rank (origin, units, direction, ctx) { + const originRect = ctx.rect(origin) + if (!originRect) return [] + const { measured } = measureAll(units.filter(unit => unit.id !== origin.id), ctx) + const ahead = measured.filter(item => isInDirection(originRect, item.rect, direction)) + return sortByDirection(originRect, ahead, direction) + }, + enter (units, direction, originRect, ctx) { + const { measured, unmeasured } = measureAll(units, ctx) + const sorted = originRect && direction + ? sortByDirection(originRect, measured, direction) + : measured.sort(readingOrder).map(item => item.unit) + return sorted.concat(unmeasured.sort(byOrder)) + } +} + +/** Walks from `index` in `step` increments, optionally continuing from the other end. */ +function walk (units: Unit[], index: number, step: 1 | -1, wrap: boolean): Unit[] { + const result: Unit[] = [] + for (let i = index + step; i >= 0 && i < units.length; i += step) result.push(units[i]) + if (wrap) { + const start = step === 1 ? 0 : units.length - 1 + for (let i = start; i !== index; i += step) result.push(units[i]) + } + return result +} + +/** Moves by position in a single row or column. Never measures the layout. */ +export const listStrategy: Strategy = { + rank (origin, units, direction, ctx) { + const sorted = units.slice().sort(byOrder) + const index = sorted.findIndex(unit => unit.id === origin.id) + const horizontal = (ctx.zone.orientation ?? 'horizontal') === 'horizontal' + const forward = horizontal ? 'right' : 'down' + const backward = horizontal ? 'left' : 'up' + if (index === -1 || (direction !== forward && direction !== backward)) return [] + return walk(sorted, index, direction === forward ? 1 : -1, !!ctx.zone.wrap) + }, + enter (units, direction, originRect, ctx) { + const horizontal = (ctx.zone.orientation ?? 'horizontal') === 'horizontal' + const alongAxis = direction === null || (direction === 'left' || direction === 'right') === horizontal + // Coming from the side (e.g. from the row above): land on the aligned item. + if (!alongAxis && originRect) return geometryStrategy.enter(units, direction, originRect, ctx) + const sorted = units.slice().sort(byOrder) + return direction === (horizontal ? 'left' : 'up') ? sorted.reverse() : sorted + } +} + +/** Moves by row/column index in a grid with `zone.columns` columns. Never measures inside the zone. */ +export const gridStrategy: Strategy = { + rank (origin, units, direction, ctx) { + const sorted = units.slice().sort(byOrder) + const index = sorted.findIndex(unit => unit.id === origin.id) + if (index === -1) return [] + const columns = Math.max(1, ctx.zone.columns ?? sorted.length) + const wrap = !!ctx.zone.wrap + + if (direction === 'left' || direction === 'right') { + const rowStart = index - (index % columns) + const row = sorted.slice(rowStart, rowStart + columns) + return walk(row, index - rowStart, direction === 'right' ? 1 : -1, wrap) + } + + const column = index % columns + const columnUnits = sorted.filter((_, i) => i % columns === column) + const result = walk(columnUnits, Math.floor(index / columns), direction === 'down' ? 1 : -1, wrap) + // The last row may be shorter: moving down into it lands on its last item. + const lastRowStart = Math.floor((sorted.length - 1) / columns) * columns + if (direction === 'down' && result.length === 0 && index < lastRowStart) { + result.push(sorted[sorted.length - 1]) + } + return result + }, + enter (units, direction, originRect, ctx) { + return originRect ? geometryStrategy.enter(units, direction, originRect, ctx) : units.slice().sort(byOrder) + } +} + +const builtIn: Record = { + geometry: geometryStrategy, + list: listStrategy, + grid: gridStrategy +} + +export function resolveStrategy (strategy: BuiltInStrategy | Strategy | undefined): Strategy { + if (!strategy) return geometryStrategy + return typeof strategy === 'string' ? builtIn[strategy] : strategy +} diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts new file mode 100644 index 0000000..2f57afa --- /dev/null +++ b/packages/core/src/types.ts @@ -0,0 +1,134 @@ +export type Direction = 'up' | 'down' | 'left' | 'right' + +/** Abstract input actions. Platform key codes are translated to these by an input binding. */ +export type Action = Direction | 'enter' | 'back' + +export interface Rect { + x: number + y: number + width: number + height: number +} + +/** Per-direction override: an element or zone id to jump to, or `null` to block the move. */ +export type DirectionMap = Partial> + +/** + * Port to the rendering environment. The core never touches the DOM: it asks the adapter + * for geometry and lets it decide how to schedule deferred work. + */ +export interface NavigationAdapter { + /** Returns the rect of an element or zone, or `null` when it cannot be measured. */ + measure(id: string, node: unknown): Rect | null + /** + * Orders two siblings that have no explicit `order` (negative when `a` comes first, 0 when + * unknown). The DOM adapter uses document order; otherwise registration order is used. + */ + compare?(a: NodeRef, b: NodeRef): number + /** Runs `task` after the current synchronous work (defaults to a microtask). */ + schedule?(task: () => void): void +} + +export interface NodeRef { + id: string + node: unknown +} + +export interface FocusableOptions { + id: string + /** Zone that contains the element. Omit to place it in the root of `layer`. */ + zone?: string + /** Layer used only when the element has no zone. Defaults to `"default"`. */ + layer?: string + disabled?: boolean + /** Position inside `list`/`grid` zones. Defaults to registration order. */ + order?: number + nextFocus?: DirectionMap + /** Opaque handle passed back to `adapter.measure` (e.g. the DOM element). */ + node?: unknown + onFocus?: (id: string) => void + onBlur?: (id: string) => void + onPress?: (id: string) => void +} + +export type BuiltInStrategy = 'geometry' | 'list' | 'grid' + +export interface ZoneOptions { + id: string + /** Parent zone. Omit to attach the zone to the root of `layer`. */ + parent?: string + /** Layer used only when the zone has no parent. Defaults to `"default"`. */ + layer?: string + strategy?: BuiltInStrategy | Strategy + /** Axis for `list` zones. Defaults to `"horizontal"`. */ + orientation?: 'horizontal' | 'vertical' + /** Column count for `grid` zones. */ + columns?: number + /** Cycle inside the zone instead of leaving it at the edges. */ + wrap?: boolean + /** Re-focus the last focused child when the zone is entered again. Defaults to `true`. */ + saveLastFocused?: boolean + /** Child (element or zone) to focus when entering and there is no remembered child. */ + firstFocus?: string + /** Never let focus leave the zone with arrow keys. */ + trap?: boolean + disabled?: boolean + order?: number + nextZone?: DirectionMap + node?: unknown + onEnter?: (id: string) => void + onLeave?: (id: string) => void +} + +export interface LayerOptions { + /** Higher priority wins. The `"default"` layer has priority 0. Defaults to 1. */ + priority?: number +} + +/** A child of a zone as seen by strategies: either a focusable element or a nested zone. */ +export interface Unit { + kind: 'element' | 'zone' + id: string + order: number +} + +export interface StrategyContext { + zone: Readonly + rect(unit: Unit): Rect | null +} + +/** + * Decides where focus goes inside one zone. `rank` returns candidates in preference order; + * the core takes the first one that can actually receive focus (so disabled or empty zones + * are skipped without the strategy knowing about them). + */ +export interface Strategy { + rank(origin: Unit, units: Unit[], direction: Direction, ctx: StrategyContext): Unit[] + /** Candidates when focus enters the zone from outside. `originRect` is where it came from. */ + enter(units: Unit[], direction: Direction | null, originRect: Rect | null, ctx: StrategyContext): Unit[] +} + +export interface NavigationState { + focusedId: string | null + activeLayer: string +} + +export interface NavigationEvents { + focus: { id: string, previousId: string | null } + blur: { id: string, nextId: string | null } + enterZone: { id: string } + leaveZone: { id: string } + press: { id: string } + back: { focusedId: string | null } + /** Emitted when a move finds nowhere to go. */ + boundary: { direction: Direction, focusedId: string | null } + layerChange: { activeLayer: string, previousLayer: string } +} + +export type EventName = keyof NavigationEvents + +export interface NavigationOptions { + adapter?: NavigationAdapter + /** Focus the first reachable element automatically when nothing is focused. Defaults to `true`. */ + autoFocus?: boolean +} diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 0000000..564a599 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts new file mode 100644 index 0000000..deb668b --- /dev/null +++ b/packages/core/tsup.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from 'tsup' +import { baseConfig } from '../../tsup.base' + +export default defineConfig(baseConfig) diff --git a/packages/dom/README.md b/packages/dom/README.md new file mode 100644 index 0000000..9316a0a --- /dev/null +++ b/packages/dom/README.md @@ -0,0 +1,17 @@ +# @arxis/gtvzone-dom + +Conecta `@arxis/gtvzone-core` con el navegador: + +- `createDomAdapter()`: mide con `getBoundingClientRect` (o tu polyfill vía `getRect`) y ordena por posición en el DOM. +- `bindKeys(nav)` / `keyMaps` / `detectPlatform()`: teclas del control remoto para Tizen, webOS, VIDAA y navegador. +- `bindZone(nav, container, { item })`: modo selectores para apps sin framework, con `MutationObserver`. +- `createDomNavigation()`: todo lo anterior en una llamada. + +```js +import { bindZone, createDomNavigation } from '@arxis/gtvzone-dom' + +const nav = createDomNavigation() +bindZone(nav, '#row', { item: '.card', strategy: 'list' }) +``` + +Documentación completa en el [README del repo](https://github.com/renearias/gtvzone#readme). diff --git a/packages/dom/package.json b/packages/dom/package.json new file mode 100644 index 0000000..2ab8e51 --- /dev/null +++ b/packages/dom/package.json @@ -0,0 +1,28 @@ +{ + "name": "@arxis/gtvzone-dom", + "version": "1.0.0-alpha.0", + "description": "Adapter de navegador para gtvzone: medición, teclas por plataforma y modo selectores para apps sin framework", + "license": "MIT", + "type": "module", + "sideEffects": false, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": ["dist"], + "keywords": ["smart-tv", "tizen", "webos", "hbbtv", "spatial-navigation", "focus"], + "repository": { "type": "git", "url": "git+https://github.com/renearias/gtvzone.git", "directory": "packages/dom" }, + "scripts": { + "build": "tsup", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "@arxis/gtvzone-core": "workspace:^" + } +} diff --git a/packages/dom/src/adapter.ts b/packages/dom/src/adapter.ts new file mode 100644 index 0000000..f2b1916 --- /dev/null +++ b/packages/dom/src/adapter.ts @@ -0,0 +1,51 @@ +import type { NavigationAdapter, NodeRef, Rect } from '@arxis/gtvzone-core' + +export interface DomAdapterOptions { + /** + * Measures an element. Override it to plug a polyfill for TV browsers where + * `getBoundingClientRect` is wrong inside transformed or scrolled containers. + */ + getRect?: (element: Element) => Rect + /** Document used to look up elements by id. Defaults to the global `document`. */ + document?: Document +} + +const isElement = (node: unknown): node is Element => + typeof Element !== 'undefined' && node instanceof Element + +const defaultGetRect = (element: Element): Rect => { + const rect = element.getBoundingClientRect() + return { x: rect.left, y: rect.top, width: rect.width, height: rect.height } +} + +/** + * Browser adapter: measures registered nodes (or looks them up by id) and orders siblings + * by document position. Elements with no size (e.g. `display: none`) are not measurable. + */ +export function createDomAdapter (options: DomAdapterOptions = {}): NavigationAdapter { + const getRect = options.getRect ?? defaultGetRect + + const resolve = ({ id, node }: NodeRef): Element | null => { + if (isElement(node)) return node + const doc = options.document ?? (typeof document !== 'undefined' ? document : undefined) + return doc?.getElementById(id) ?? null + } + + return { + measure (id, node) { + const element = resolve({ id, node }) + if (!element) return null + const rect = getRect(element) + return rect.width === 0 && rect.height === 0 ? null : rect + }, + compare (a, b) { + const first = resolve(a) + const second = resolve(b) + if (!first || !second || first === second) return 0 + const position = first.compareDocumentPosition(second) + if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1 + if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1 + return 0 + } + } +} diff --git a/packages/dom/src/dom.test.ts b/packages/dom/src/dom.test.ts new file mode 100644 index 0000000..b0352b4 --- /dev/null +++ b/packages/dom/src/dom.test.ts @@ -0,0 +1,157 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import { createNavigation } from '@arxis/gtvzone-core' +import type { Rect } from '@arxis/gtvzone-core' +import { bindKeys, bindZone, createDomAdapter, detectPlatform, keyMaps } from './index' + +/** jsdom has no layout: rects come from `data-rect="x,y,w,h"`. */ +const getRect = (element: Element): Rect => { + const [x, y, width, height] = (element.getAttribute('data-rect') ?? '0,0,0,0').split(',').map(Number) + return { x, y, width, height } +} + +const flush = () => new Promise(resolve => setTimeout(resolve, 0)) + +function createNav () { + return createNavigation({ adapter: createDomAdapter({ getRect }) }) +} + +afterEach(() => { document.body.innerHTML = '' }) + +describe('createDomAdapter', () => { + it('measures the registered node, falls back to the id and ignores hidden elements', () => { + document.body.innerHTML = '
' + const adapter = createDomAdapter({ getRect }) + const a = document.getElementById('a') + expect(adapter.measure('ignored', a)).toEqual({ x: 1, y: 2, width: 3, height: 4 }) + expect(adapter.measure('a', undefined)).toEqual({ x: 1, y: 2, width: 3, height: 4 }) + expect(adapter.measure('hidden', undefined)).toBeNull() + expect(adapter.measure('missing', undefined)).toBeNull() + }) + + it('orders nodes by document position', () => { + document.body.innerHTML = '' + const adapter = createDomAdapter() + expect(adapter.compare!({ id: 'second', node: null }, { id: 'first', node: null })).toBe(1) + expect(adapter.compare!({ id: 'first', node: null }, { id: 'second', node: null })).toBe(-1) + }) +}) + +describe('keys', () => { + it('detects the platform from the user agent', () => { + expect(detectPlatform('Mozilla/5.0 (SMART-TV; Linux; Tizen 5.0)')).toBe('tizen') + expect(detectPlatform('Mozilla/5.0 (Web0S; Linux/SmartTV)')).toBe('webos') + expect(detectPlatform('Mozilla/5.0 VIDAA/6.0')).toBe('hisense') + expect(detectPlatform('Mozilla/5.0 (Macintosh)')).toBe('browser') + }) + + it('maps keys to actions, prevents default and can be unbound', () => { + const nav = createNavigation() + const move = vi.spyOn(nav, 'handleAction') + const onBack = vi.fn() + nav.on('back', onBack) + const unbind = bindKeys(nav, { keyMap: keyMaps.tizen }) + + const right = new KeyboardEvent('keydown', { keyCode: 39, cancelable: true }) + window.dispatchEvent(right) + expect(move).toHaveBeenCalledWith('right') + expect(right.defaultPrevented).toBe(true) + + window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 10009 })) + expect(onBack).toHaveBeenCalledTimes(1) + + unbind() + window.dispatchEvent(new KeyboardEvent('keydown', { keyCode: 39 })) + expect(move).toHaveBeenCalledTimes(2) + }) +}) + +describe('bindZone', () => { + function renderRow () { + document.body.innerHTML = ` +
    +
  • +
  • +
` + } + + it('registers the items and toggles the focused class', async () => { + renderRow() + const nav = createNav() + bindZone(nav, '#row', { item: '.card', strategy: 'list' }) + await flush() + expect(nav.getFocusedId()).toBe('a') + expect(document.getElementById('a')!.classList.contains('gtv-focused')).toBe(true) + + nav.move('right') + expect(document.getElementById('a')!.classList.contains('gtv-focused')).toBe(false) + expect(document.getElementById('b')!.classList.contains('gtv-focused')).toBe(true) + }) + + it('follows DOM mutations', async () => { + renderRow() + const nav = createNav() + bindZone(nav, '#row', { item: '.card', strategy: 'list' }) + await flush() + + const extra = document.createElement('li') + extra.className = 'card' + document.getElementById('row')!.appendChild(extra) + await flush() + nav.move('right') + nav.move('right') + expect(nav.getFocusedId()).toBe(extra.id) + + extra.remove() + await flush() + expect(nav.getFocusedId()).not.toBe(extra.id) + + document.getElementById('b')!.setAttribute('disabled', '') + await flush() + nav.move('left') + expect(nav.move('right')).toBe(false) + }) + + it('dispatches a press event on the focused item', async () => { + renderRow() + const nav = createNav() + const onPress = vi.fn() + bindZone(nav, '#row', { item: '.card', onPress }) + await flush() + const listener = vi.fn() + document.body.addEventListener('gtvzone:press', listener) + nav.press() + expect(onPress).toHaveBeenCalledWith(document.getElementById('a')) + expect((listener.mock.calls[0][0] as CustomEvent).detail).toEqual({ id: 'a' }) + }) + + it('nests zones bound inside other zones', async () => { + document.body.innerHTML = ` +
+ +
    +
  • +
  • +
+
` + const nav = createNav() + bindZone(nav, '#page', { item: '.item' }) + bindZone(nav, '#row', { item: '.item', strategy: 'list', trap: true }) + await flush() + expect(nav.getFocusedId()).toBe('menu') + nav.move('down') + expect(nav.getFocusedId()).toBe('c1') + expect(nav.move('up')).toBe(false) + }) + + it('cleans up on destroy', async () => { + renderRow() + const nav = createNav() + const binding = bindZone(nav, '#row', { item: '.card' }) + await flush() + binding.destroy() + await flush() + expect(nav.getFocusedId()).toBeNull() + expect(document.querySelector('.gtv-focused')).toBeNull() + expect(document.getElementById('row')!.hasAttribute('data-gtvzone')).toBe(false) + }) +}) diff --git a/packages/dom/src/index.ts b/packages/dom/src/index.ts new file mode 100644 index 0000000..e46d39c --- /dev/null +++ b/packages/dom/src/index.ts @@ -0,0 +1,32 @@ +import { createNavigation } from '@arxis/gtvzone-core' +import type { Navigation, NavigationOptions } from '@arxis/gtvzone-core' +import { createDomAdapter } from './adapter' +import type { DomAdapterOptions } from './adapter' +import { bindKeys } from './keys' +import type { BindKeysOptions } from './keys' + +export { createDomAdapter } from './adapter' +export type { DomAdapterOptions } from './adapter' +export { bindKeys, detectPlatform, keyMaps } from './keys' +export type { BindKeysOptions, KeyMap, Platform } from './keys' +export { bindZone } from './zone' +export type { BindZoneOptions, ZoneBinding } from './zone' + +export interface DomNavigationOptions extends Omit { + adapter?: DomAdapterOptions + /** Key binding options, or `false` to wire input yourself. */ + keys?: BindKeysOptions | false +} + +/** Navigation instance wired to the browser: DOM adapter plus remote-control keys. */ +export function createDomNavigation (options: DomNavigationOptions = {}): Navigation { + const { adapter, keys, ...navigationOptions } = options + const nav = createNavigation({ ...navigationOptions, adapter: createDomAdapter(adapter) }) + const unbindKeys = keys === false ? undefined : bindKeys(nav, keys) + const destroy = nav.destroy + nav.destroy = () => { + unbindKeys?.() + destroy() + } + return nav +} diff --git a/packages/dom/src/keys.ts b/packages/dom/src/keys.ts new file mode 100644 index 0000000..70c9fcc --- /dev/null +++ b/packages/dom/src/keys.ts @@ -0,0 +1,57 @@ +import type { Action, Navigation } from '@arxis/gtvzone-core' + +export type Platform = 'tizen' | 'webos' | 'hisense' | 'browser' + +/** keyCode → action. */ +export type KeyMap = Record + +const baseKeys: KeyMap = { 37: 'left', 38: 'up', 39: 'right', 40: 'down', 13: 'enter' } + +export const keyMaps: Record = { + tizen: { ...baseKeys, 10009: 'back' }, + webos: { ...baseKeys, 461: 'back' }, + hisense: { ...baseKeys, 8: 'back' }, + browser: { ...baseKeys, 27: 'back', 8: 'back' } +} + +/** Keys reported only through `KeyboardEvent.key` by some emulators and desktop browsers. */ +const keyNames: Record = { + ArrowLeft: 'left', ArrowUp: 'up', ArrowRight: 'right', ArrowDown: 'down', Enter: 'enter' +} + +export function detectPlatform (userAgent: string = typeof navigator !== 'undefined' ? navigator.userAgent : ''): Platform { + if (/Tizen/i.test(userAgent)) return 'tizen' + if (/Web0S|webOS|NetCast/i.test(userAgent)) return 'webos' + if (/VIDAA|Hisense/i.test(userAgent)) return 'hisense' + return 'browser' +} + +export interface BindKeysOptions { + /** Defaults to the key map of the detected platform. */ + keyMap?: KeyMap + /** Defaults to `window`. */ + target?: Pick + /** Prevent the browser default (e.g. page scroll) for mapped keys. Defaults to `true`. */ + preventDefault?: boolean +} + +const isArrow = (action: Action) => action !== 'enter' && action !== 'back' + +/** Translates key presses into navigation actions. Returns a function that removes the listener. */ +export function bindKeys (nav: Navigation, options: BindKeysOptions = {}): () => void { + const keyMap = options.keyMap ?? keyMaps[detectPlatform()] + const target = options.target ?? window + const preventDefault = options.preventDefault !== false + + const onKeyDown = (event: Event) => { + const { keyCode, key } = event as KeyboardEvent + const action = keyMap[keyCode] ?? keyNames[key] + if (!action) return + const handled = nav.handleAction(action) + // Arrows are always swallowed so a boundary never scrolls the page. + if (preventDefault && (handled || isArrow(action))) event.preventDefault() + } + + target.addEventListener('keydown', onKeyDown) + return () => target.removeEventListener('keydown', onKeyDown) +} diff --git a/packages/dom/src/zone.ts b/packages/dom/src/zone.ts new file mode 100644 index 0000000..4ff5419 --- /dev/null +++ b/packages/dom/src/zone.ts @@ -0,0 +1,117 @@ +import type { Navigation, ZoneOptions } from '@arxis/gtvzone-core' + +const ZONE_ATTRIBUTE = 'data-gtvzone' +// Marks which zone owns an item, so a parent zone never unregisters items a nested zone claimed. +const OWNER_ATTRIBUTE = 'data-gtvzone-item' +const PRESS_EVENT = 'gtvzone:press' + +let idCounter = 0 +const nextId = (prefix: string) => `${prefix}-${++idCounter}` + +export interface BindZoneOptions extends Omit { + /** Zone id. Defaults to the container id, or a generated one. */ + id?: string + /** Selector of the focusable items inside the container. */ + item: string + /** Class toggled on the focused item. Defaults to `"gtv-focused"`. */ + focusedClass?: string + /** Also move the browser focus (useful for screen readers). Defaults to `false`. */ + nativeFocus?: boolean + onPress?: (element: Element) => void +} + +export interface ZoneBinding { + id: string + /** Re-scans the container. Mutations are picked up automatically; call it after changes the observer cannot see. */ + refresh(): void + destroy(): void +} + +const isDisabled = (element: Element) => + element.hasAttribute('disabled') || element.getAttribute('aria-disabled') === 'true' + +/** + * Selector mode for apps without a framework: turns a container into a zone and keeps its + * items registered as the DOM changes. Nested bound containers become child zones. + */ +export function bindZone (nav: Navigation, container: Element | string, options: BindZoneOptions): ZoneBinding { + const root = typeof container === 'string' ? document.querySelector(container) : container + if (!root) throw new Error(`gtvzone: container not found: ${String(container)}`) + + const { item, focusedClass = 'gtv-focused', nativeFocus = false, onPress, id, ...zoneOptions } = options + if (!root.id) root.id = id ?? nextId('gtvzone') + const zoneId = id ?? root.id + root.setAttribute(ZONE_ATTRIBUTE, zoneId) + + const parentZone = root.parentElement?.closest(`[${ZONE_ATTRIBUTE}]`)?.getAttribute(ZONE_ATTRIBUTE) ?? undefined + nav.registerZone({ parent: parentZone, ...zoneOptions, id: zoneId, node: root }) + + const registered = new Map() + + const ownItems = () => Array.prototype.filter.call( + root.querySelectorAll(item), + (element: Element) => element.parentElement?.closest(`[${ZONE_ATTRIBUTE}]`) === root + ) as Element[] + + function registerItem (element: Element) { + if (!element.id) element.id = nextId('gtvzone-item') + const itemId = element.id + registered.set(element, itemId) + element.setAttribute(OWNER_ATTRIBUTE, zoneId) + nav.register({ + id: itemId, + zone: zoneId, + node: element, + disabled: isDisabled(element), + onFocus: () => { + element.classList.add(focusedClass) + if (nativeFocus && element instanceof HTMLElement) element.focus() + }, + onBlur: () => element.classList.remove(focusedClass), + onPress: () => { + element.dispatchEvent(new CustomEvent(PRESS_EVENT, { bubbles: true, detail: { id: itemId } })) + onPress?.(element) + } + }) + } + + function release (element: Element, itemId: string) { + registered.delete(element) + if (element.getAttribute(OWNER_ATTRIBUTE) !== zoneId) return + element.removeAttribute(OWNER_ATTRIBUTE) + element.classList.remove(focusedClass) + nav.unregister(itemId) + } + + function refresh () { + const current = ownItems() + registered.forEach((itemId, element) => { + if (current.indexOf(element) === -1) release(element, itemId) + }) + current.forEach(element => { + const itemId = registered.get(element) + if (itemId === undefined) registerItem(element) + else nav.update(itemId, { disabled: isDisabled(element) }) + }) + } + + refresh() + const observer = new MutationObserver(refresh) + observer.observe(root, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ['disabled', 'aria-disabled', ZONE_ATTRIBUTE] + }) + + return { + id: zoneId, + refresh, + destroy () { + observer.disconnect() + registered.forEach((itemId, element) => release(element, itemId)) + nav.unregisterZone(zoneId) + root.removeAttribute(ZONE_ATTRIBUTE) + } + } +} diff --git a/packages/dom/tsconfig.json b/packages/dom/tsconfig.json new file mode 100644 index 0000000..564a599 --- /dev/null +++ b/packages/dom/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} diff --git a/packages/dom/tsup.config.ts b/packages/dom/tsup.config.ts new file mode 100644 index 0000000..deb668b --- /dev/null +++ b/packages/dom/tsup.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from 'tsup' +import { baseConfig } from '../../tsup.base' + +export default defineConfig(baseConfig) diff --git a/packages/react/README.md b/packages/react/README.md new file mode 100644 index 0000000..16f083a --- /dev/null +++ b/packages/react/README.md @@ -0,0 +1,11 @@ +# @arxis/gtvzone-react + +Bindings de React (≥ 16.8) para gtvzone: `NavigationProvider`, `useFocusable`, ``, +`` y selectores de estado (`useFocusedId`, `useIsFocused`, `useActiveLayer`). + +```jsx +const { ref, focused } = useFocusable({ id: 'play', onPress: play }) +return +``` + +Documentación completa en el [README del repo](https://github.com/renearias/gtvzone#readme). diff --git a/packages/react/package.json b/packages/react/package.json new file mode 100644 index 0000000..a1fe23a --- /dev/null +++ b/packages/react/package.json @@ -0,0 +1,40 @@ +{ + "name": "@arxis/gtvzone-react", + "version": "1.0.0-alpha.0", + "description": "Hooks y componentes de React para la navegación espacial de gtvzone", + "license": "MIT", + "type": "module", + "sideEffects": false, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "files": ["dist"], + "keywords": ["react", "smart-tv", "tizen", "webos", "spatial-navigation", "focus"], + "repository": { "type": "git", "url": "git+https://github.com/renearias/gtvzone.git", "directory": "packages/react" }, + "scripts": { + "build": "tsup", + "typecheck": "tsc --noEmit -p tsconfig.json" + }, + "dependencies": { + "@arxis/gtvzone-core": "workspace:^", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "devDependencies": { + "@arxis/gtvzone-dom": "workspace:^", + "@testing-library/react": "^16.3.0", + "@types/react": "^19.1.0", + "@types/use-sync-external-store": "^1.5.0", + "react": "^19.1.0", + "react-dom": "^19.1.0" + } +} diff --git a/packages/react/src/FocusLayer.tsx b/packages/react/src/FocusLayer.tsx new file mode 100644 index 0000000..306ab07 --- /dev/null +++ b/packages/react/src/FocusLayer.tsx @@ -0,0 +1,27 @@ +import { useEffect } from 'react' +import type { ReactNode } from 'react' +import type { LayerOptions } from '@arxis/gtvzone-core' +import { LayerContext, useNavigation } from './context' + +/** Opens `id` as the active layer while `active` is true; focus returns where it was when it closes. */ +export function useFocusLayer (id: string, active = true, options: LayerOptions = {}) { + const navigation = useNavigation() + const { priority } = options + useEffect(() => { + if (!active) return + navigation.openLayer(id, { priority }) + return () => navigation.closeLayer(id) + }, [navigation, id, active, priority]) +} + +export interface FocusLayerProps extends LayerOptions { + id: string + active?: boolean + children?: ReactNode +} + +/** Isolates its children (e.g. a modal): arrows cannot reach other layers while it is open. */ +export function FocusLayer ({ id, active = true, priority, children }: FocusLayerProps) { + useFocusLayer(id, active, { priority }) + return {children} +} diff --git a/packages/react/src/FocusZone.tsx b/packages/react/src/FocusZone.tsx new file mode 100644 index 0000000..0a37069 --- /dev/null +++ b/packages/react/src/FocusZone.tsx @@ -0,0 +1,67 @@ +import { forwardRef, useCallback, useContext, useEffect, useRef } from 'react' +import type { ElementType, HTMLAttributes } from 'react' +import type { ZoneOptions } from '@arxis/gtvzone-core' +import { LayerContext, useNavigation, ZoneContext } from './context' +import { assignRef, useLatest, useShallowStable, useStableId } from './utils' + +type ZoneProps = Omit + +export interface FocusZoneProps extends ZoneProps, Omit, keyof ZoneProps | 'id'> { + /** Zone id, also set as the DOM id. Defaults to a generated id (not rendered). */ + id?: string + /** Element or component to render. Defaults to `"div"`. */ + as?: ElementType +} + +/** Groups focusable children into a zone. Zones nest: the closest `` is the parent. */ +export const FocusZone = forwardRef(function FocusZone (props, forwardedRef) { + const { + id: idProp, as = 'div', strategy, orientation, columns, wrap, saveLastFocused, firstFocus, + trap, disabled, order, nextZone, onEnter, onLeave, children, ...rest + } = props + const navigation = useNavigation() + const parent = useContext(ZoneContext) + const layer = useContext(LayerContext) + const id = useStableId('gtvzone', idProp) + const latest = useLatest({ onEnter, onLeave }) + const stableNextZone = useShallowStable(nextZone) + const nodeRef = useRef(null) + const registered = useRef(false) + + const options = { strategy, orientation, columns, wrap, saveLastFocused, firstFocus, trap, disabled, order, nextZone: stableNextZone } + const latestOptions = useLatest(options) + + useEffect(() => { + const unregister = navigation.registerZone({ + ...latestOptions.current, + id, + parent, + layer, + node: nodeRef.current, + onEnter: zoneId => latest.current.onEnter?.(zoneId), + onLeave: zoneId => latest.current.onLeave?.(zoneId) + }) + registered.current = true + return () => { + registered.current = false + unregister() + } + }, [navigation, id, parent, layer, latest, latestOptions]) + + useEffect(() => { + if (registered.current) navigation.updateZone(id, latestOptions.current) + }, [navigation, id, latestOptions, strategy, orientation, columns, wrap, saveLastFocused, firstFocus, trap, disabled, order, stableNextZone]) + + const ref = useCallback((node: HTMLElement | null) => { + nodeRef.current = node + assignRef(forwardedRef, node) + if (registered.current) navigation.updateZone(id, { node }) + }, [navigation, id, forwardedRef]) + + const Component = as + return ( + + {children} + + ) +}) diff --git a/packages/react/src/context.tsx b/packages/react/src/context.tsx new file mode 100644 index 0000000..ba1c3fa --- /dev/null +++ b/packages/react/src/context.tsx @@ -0,0 +1,39 @@ +import { createContext, useContext } from 'react' +import type { ReactNode } from 'react' +import { useSyncExternalStore } from 'use-sync-external-store/shim' +import type { Navigation, NavigationState } from '@arxis/gtvzone-core' + +const NavigationContext = createContext(null) +/** Id of the closest enclosing zone. */ +export const ZoneContext = createContext(undefined) +/** Id of the closest enclosing layer. */ +export const LayerContext = createContext(undefined) + +export interface NavigationProviderProps { + navigation: Navigation + children?: ReactNode +} + +export function NavigationProvider ({ navigation, children }: NavigationProviderProps) { + return {children} +} + +export function useNavigation (): Navigation { + const navigation = useContext(NavigationContext) + if (!navigation) throw new Error('gtvzone: wrap your app in ') + return navigation +} + +/** + * Subscribes to a slice of the navigation state. Return a primitive from `selector` so the + * component only re-renders when that slice changes. + */ +export function useNavigationState (selector: (state: NavigationState) => T): T { + const navigation = useNavigation() + const getSnapshot = () => selector(navigation.getState()) + return useSyncExternalStore(navigation.subscribe, getSnapshot, getSnapshot) +} + +export const useFocusedId = () => useNavigationState(state => state.focusedId) +export const useIsFocused = (id: string) => useNavigationState(state => state.focusedId === id) +export const useActiveLayer = () => useNavigationState(state => state.activeLayer) diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts new file mode 100644 index 0000000..c5b7347 --- /dev/null +++ b/packages/react/src/index.ts @@ -0,0 +1,15 @@ +export { + NavigationProvider, + useNavigation, + useNavigationState, + useFocusedId, + useIsFocused, + useActiveLayer +} from './context' +export type { NavigationProviderProps } from './context' +export { useFocusable } from './useFocusable' +export type { UseFocusableOptions, UseFocusableResult } from './useFocusable' +export { FocusZone } from './FocusZone' +export type { FocusZoneProps } from './FocusZone' +export { FocusLayer, useFocusLayer } from './FocusLayer' +export type { FocusLayerProps } from './FocusLayer' diff --git a/packages/react/src/react.test.tsx b/packages/react/src/react.test.tsx new file mode 100644 index 0000000..a784abc --- /dev/null +++ b/packages/react/src/react.test.tsx @@ -0,0 +1,143 @@ +import { StrictMode, useState } from 'react' +import type { ReactNode } from 'react' +import { act, cleanup, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { createNavigation } from '@arxis/gtvzone-core' +import type { Navigation, Rect } from '@arxis/gtvzone-core' +import { createDomAdapter } from '@arxis/gtvzone-dom' +import { FocusLayer, FocusZone, NavigationProvider, useFocusable } from './index' + +// Tell React this is an act() environment so state updates are flushed synchronously. +;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + +const getRect = (element: Element): Rect => { + const [x, y, width, height] = (element.getAttribute('data-rect') ?? '0,0,0,0').split(',').map(Number) + return { x, y, width, height } +} + +function Card ({ id, rect, onPress }: { id: string, rect: string, onPress?: () => void }) { + const { ref, focused } = useFocusable({ id, onPress }) + return
+} + +async function mount (ui: ReactNode, { strict = false } = {}) { + const nav = createNavigation({ adapter: createDomAdapter({ getRect }) }) + const tree = {ui} + await act(async () => { render(strict ? {tree} : tree) }) + return nav +} + +const focusedTestId = () => document.querySelector('.focused')?.getAttribute('data-testid') ?? null +const press = (nav: Navigation, action: Parameters[0]) => + act(() => { nav.handleAction(action) }) + +afterEach(cleanup) + +function Rows () { + return ( + <> + + + + + + + + + + ) +} + +describe('React bindings', () => { + it('auto focuses and re-renders only from navigation state', async () => { + const nav = await mount() + expect(focusedTestId()).toBe('a1') + await press(nav, 'right') + expect(focusedTestId()).toBe('a2') + await press(nav, 'down') + expect(focusedTestId()).toBe('b2') + }) + + it('renders zones as DOM elements with the given id and forwards refs', async () => { + const ref = { current: null as HTMLElement | null } + await mount() + expect(ref.current?.tagName).toBe('SECTION') + expect(ref.current?.id).toBe('grid') + }) + + it('works under StrictMode double effects without losing order', async () => { + const nav = await mount(, { strict: true }) + expect(focusedTestId()).toBe('a1') + await press(nav, 'right') + expect(focusedTestId()).toBe('a2') + }) + + it('calls the latest onPress', async () => { + const onPress = vi.fn() + const nav = await mount() + await press(nav, 'enter') + expect(onPress).toHaveBeenCalledTimes(1) + }) + + it('moves focus into a layer and back when it closes', async () => { + let setOpen: (open: boolean) => void = () => {} + function App () { + const [open, set] = useState(false) + setOpen = set + return ( + <> + + {open && ( + + + + + + + )} + + ) + } + const nav = await mount() + await press(nav, 'down') + expect(focusedTestId()).toBe('b1') + + await act(async () => { setOpen(true) }) + expect(focusedTestId()).toBe('ok') + await press(nav, 'down') + expect(focusedTestId()).toBe('cancel') + await press(nav, 'left') + expect(focusedTestId()).toBe('cancel') + + await act(async () => { setOpen(false) }) + expect(focusedTestId()).toBe('b1') + }) + + it('refocuses when the focused element unmounts', async () => { + let remove: () => void = () => {} + function List () { + const [items, setItems] = useState(['x1', 'x2', 'x3']) + remove = () => setItems(['x1', 'x3']) + return ( + + {items.map((id, i) => )} + + ) + } + const nav = await mount() + await press(nav, 'right') + expect(focusedTestId()).toBe('x2') + await act(async () => { remove() }) + expect(focusedTestId()).toBe('x1') + }) + + it('throws a helpful error without a provider', () => { + vi.spyOn(console, 'error').mockImplementation(() => {}) + expect(() => render()).toThrow(/NavigationProvider/) + }) +}) + +it('keeps a stable DOM id only when one is given', async () => { + await mount() + expect(screen.getByTestId('anon').id).toBe('') +}) diff --git a/packages/react/src/useFocusable.ts b/packages/react/src/useFocusable.ts new file mode 100644 index 0000000..17a6159 --- /dev/null +++ b/packages/react/src/useFocusable.ts @@ -0,0 +1,67 @@ +import { useCallback, useContext, useEffect, useRef } from 'react' +import type { FocusableOptions } from '@arxis/gtvzone-core' +import { LayerContext, useIsFocused, useNavigation, ZoneContext } from './context' +import { useLatest, useShallowStable, useStableId } from './utils' + +export interface UseFocusableOptions extends Omit { + /** Defaults to a generated id. */ + id?: string +} + +export interface UseFocusableResult { + id: string + /** Attach to the element so it can be measured. */ + ref: (node: T | null) => void + focused: boolean + focus: () => boolean +} + +/** + * Registers an element for remote-control navigation inside the closest `` / + * ``. The hook never touches the DOM: style the element from `focused`. + */ +export function useFocusable (options: UseFocusableOptions = {}): UseFocusableResult { + const navigation = useNavigation() + const zone = useContext(ZoneContext) + const layer = useContext(LayerContext) + const id = useStableId('gtvzone-item', options.id) + const latest = useLatest(options) + const nextFocus = useShallowStable(options.nextFocus) + const nodeRef = useRef(null) + const registered = useRef(false) + const { disabled, order } = options + + useEffect(() => { + const { disabled, order, nextFocus } = latest.current + const unregister = navigation.register({ + id, + zone, + layer, + node: nodeRef.current, + disabled, + order, + nextFocus, + onFocus: focusedId => latest.current.onFocus?.(focusedId), + onBlur: blurredId => latest.current.onBlur?.(blurredId), + onPress: pressedId => latest.current.onPress?.(pressedId) + }) + registered.current = true + return () => { + registered.current = false + unregister() + } + }, [navigation, id, zone, layer, latest]) + + useEffect(() => { + if (registered.current) navigation.update(id, { disabled, order, nextFocus }) + }, [navigation, id, disabled, order, nextFocus]) + + const ref = useCallback((node: T | null) => { + nodeRef.current = node + if (registered.current) navigation.update(id, { node }) + }, [navigation, id]) + + const focus = useCallback(() => navigation.setFocus(id), [navigation, id]) + + return { id, ref, focused: useIsFocused(id), focus } +} diff --git a/packages/react/src/utils.ts b/packages/react/src/utils.ts new file mode 100644 index 0000000..bc178cb --- /dev/null +++ b/packages/react/src/utils.ts @@ -0,0 +1,39 @@ +import { useEffect, useRef } from 'react' +import type { MutableRefObject, Ref } from 'react' + +let idCounter = 0 + +/** Stable generated id (React 17 has no `useId`). */ +export function useStableId (prefix: string, explicit?: string): string { + const generated = useRef(undefined) + if (!generated.current) generated.current = `${prefix}-${++idCounter}` + return explicit ?? generated.current +} + +/** Keeps the latest value in a ref so effects can call fresh callbacks without re-running. */ +export function useLatest (value: T): MutableRefObject { + const ref = useRef(value) + useEffect(() => { ref.current = value }) + return ref +} + +function shallowEqual (a: unknown, b: unknown): boolean { + if (Object.is(a, b)) return true + if (!a || !b || typeof a !== 'object' || typeof b !== 'object') return false + const keysA = Object.keys(a) + const keysB = Object.keys(b) + return keysA.length === keysB.length && + keysA.every(key => Object.is((a as Record)[key], (b as Record)[key])) +} + +/** Returns the previous reference while the value is shallowly equal, so inline objects don't retrigger effects. */ +export function useShallowStable (value: T): T { + const ref = useRef(value) + if (!shallowEqual(ref.current, value)) ref.current = value + return ref.current +} + +export function assignRef (ref: Ref | undefined, value: T | null) { + if (typeof ref === 'function') ref(value) + else if (ref) (ref as MutableRefObject).current = value +} diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json new file mode 100644 index 0000000..564a599 --- /dev/null +++ b/packages/react/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} diff --git a/packages/react/tsup.config.ts b/packages/react/tsup.config.ts new file mode 100644 index 0000000..c3b8d1e --- /dev/null +++ b/packages/react/tsup.config.ts @@ -0,0 +1,4 @@ +import { defineConfig } from 'tsup' +import { baseConfig } from '../../tsup.base' + +export default defineConfig({ ...baseConfig, external: ['react'] }) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..687dc73 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,3780 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + jsdom: + specifier: ^27.0.0 + version: 27.4.0 + tsup: + specifier: ^8.5.1 + version: 8.5.1(postcss@8.5.28)(typescript@6.0.3) + typescript: + specifier: ~6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.0.0 + version: 4.1.11(jsdom@27.4.0)(vite@8.3.0(esbuild@0.27.7)(less@4.9.1)(sass@1.104.1)) + + packages/angular: + dependencies: + '@arxis/gtvzone-core': + specifier: workspace:^ + version: link:../core + '@arxis/gtvzone-dom': + specifier: workspace:^ + version: link:../dom + tslib: + specifier: ^2.8.0 + version: 2.8.1 + devDependencies: + '@angular/common': + specifier: ~22.1.0 + version: 22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2) + '@angular/compiler': + specifier: ~22.1.0 + version: 22.1.7 + '@angular/compiler-cli': + specifier: ~22.1.0 + version: 22.1.7(@angular/compiler@22.1.7)(typescript@6.0.3) + '@angular/core': + specifier: ~22.1.0 + version: 22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2) + '@angular/platform-browser': + specifier: ~22.1.0 + version: 22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2)) + ng-packagr: + specifier: ~22.1.0 + version: 22.1.1(@angular/compiler-cli@22.1.7(@angular/compiler@22.1.7)(typescript@6.0.3))(tslib@2.8.1)(typescript@6.0.3) + rxjs: + specifier: ^7.8.0 + version: 7.8.2 + + packages/core: {} + + packages/dom: + dependencies: + '@arxis/gtvzone-core': + specifier: workspace:^ + version: link:../core + + packages/react: + dependencies: + '@arxis/gtvzone-core': + specifier: workspace:^ + version: link:../core + use-sync-external-store: + specifier: ^1.5.0 + version: 1.7.0(react@19.3.0) + devDependencies: + '@arxis/gtvzone-dom': + specifier: workspace:^ + version: link:../dom + '@testing-library/react': + specifier: ^16.3.0 + version: 16.3.3(@testing-library/dom@10.4.2)(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) + '@types/react': + specifier: ^19.1.0 + version: 19.3.0 + '@types/use-sync-external-store': + specifier: ^1.5.0 + version: 1.7.0 + react: + specifier: ^19.1.0 + version: 19.3.0 + react-dom: + specifier: ^19.1.0 + version: 19.3.0(react@19.3.0) + +packages: + + '@acemir/cssom@0.9.31': + resolution: {integrity: sha512-ZnR3GSaH+/vJ0YlHau21FjfLYjMpYVIzTD8M8vIEQvIGxeOXyXdzCI140rrCY862p/C/BbzWsjc1dgnM9mkoTA==} + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@angular/common@22.1.7': + resolution: {integrity: sha512-SLW4AdH2Y8G9SM/iFRNpCQtkmvq55RcIQWsADT9ypZ7FxxcnJRsr4Q/WnbUBbuCslsCkrgzU1i9WDiW0m7gcnw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + peerDependencies: + '@angular/core': 22.1.7 + rxjs: ^6.5.3 || ^7.4.0 + + '@angular/compiler-cli@22.1.7': + resolution: {integrity: sha512-VGg8Kyt0YymQfnUUFk79vFaM+x5xmIMHy+BZh3P2Lh/uxqCXTWhJJH8kOHKtIGGH5/wKhBEmSy2CezdcIuM9xg==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@angular/compiler': 22.1.7 + typescript: '>=6.0 <6.1' + peerDependenciesMeta: + typescript: + optional: true + + '@angular/compiler@22.1.7': + resolution: {integrity: sha512-qhoHAByh805nT4Xy4Wd2kE5FvkTc7wcg8Qps+EH6+z4KRfwGiL5cwdgooGWjTAldFCeqTgJ0BO/0dB6GHZjdkQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + + '@angular/core@22.1.7': + resolution: {integrity: sha512-T9bt1PIgoN7n+TxGHTUgu0EsomlGMQ84YMXbMB+N6HhD8C1qZxn0uCbnAtCUjcecg1am8ioKxNTO0eII9ouioQ==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + peerDependencies: + '@angular/compiler': 22.1.7 + rxjs: ^6.5.3 || ^7.4.0 + zone.js: ~0.15.0 || ~0.16.0 + peerDependenciesMeta: + '@angular/compiler': + optional: true + zone.js: + optional: true + + '@angular/platform-browser@22.1.7': + resolution: {integrity: sha512-4dT3qGw3ZHVQUcHKALzMx9MTo+pxAMzHIPxc8HUESklqXfq6GRaIIfFMvE0RsquI9iqu6admD6lOAEzLWS8DxA==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + peerDependencies: + '@angular/animations': 22.1.7 + '@angular/common': 22.1.7 + '@angular/core': 22.1.7 + peerDependenciesMeta: + '@angular/animations': + optional: true + + '@asamuzakjp/css-color@4.1.2': + resolution: {integrity: sha512-NfBUvBaYgKIuq6E/RBLY1m0IohzNHAYyaJGuTK79Z23uNwmz2jl1mPsC5ZxCCxylinKhT1Amn5oNTlx1wN8cQg==} + + '@asamuzakjp/dom-selector@6.8.1': + resolution: {integrity: sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/code-frame@8.0.6': + resolution: {integrity: sha512-uMHhvmDmwFiLuiozaoRYTZHqMFuu3fem/X9JY78lohAuLoh8LSGGC8JqUzo2cVuBsBxkjMZNk56CE7ock8Xxkw==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/compat-data@8.0.5': + resolution: {integrity: sha512-YLsYoQMvL8l8WrGpN3Zj7O1wK5LEBN+cQtux7BcuHyxIXve724XG+zuJ1n3U1cUweRtTzQOA4IHbuQw3N34SZw==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/core@8.0.1': + resolution: {integrity: sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/generator@8.0.6': + resolution: {integrity: sha512-/pcanjdRsonCexCuxgGD8fKnBLLzk2i9uM4kmcmGyyev2R5YDs+rafMYZ89RvaGY1mW8T7C5au0TyYaEeHBGng==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-compilation-targets@8.0.6': + resolution: {integrity: sha512-HTrtZ4+iA0+kzdbCgvdSk1U4pEl3BvpGnlUbualCGNbrVhhQvhgBzurTUmqpWyvyHbWYWaIfdx2mOlL9LJjsew==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-globals@8.0.6': + resolution: {integrity: sha512-OdnA/N52LhzFpeUuVz5YGuSWjc7TPHiFmVYZbPlSgExocO2GwbxVpSni9MVs5wwW+wZ4kPuwBEBN9pIPDYz1mA==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-string-parser@8.0.6': + resolution: {integrity: sha512-+6HYEBnsDRZHJtUTAEfMQd0zMDqdFHS7C+5vAsEZqBUr3vEMWOWlHJ0Jz1gS8jJIlCvLsuA6ivmeoqzH936YcA==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@8.0.6': + resolution: {integrity: sha512-HYx0YmuchRxRSpnjvcNffnPS8DCauKNmnumIuHG3jnQAVewRA7kfJGwF13ayEpSZs7s4R/oBH4eppuXcAQd5JQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helper-validator-option@8.0.0': + resolution: {integrity: sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/helpers@8.0.5': + resolution: {integrity: sha512-fQtPOXjYOYv85PIdwotp2TJGVYOycX0PQq+l844fFAxOULtBy8BVF35GyeueX0r4KvDthqPH5xAI1clQPk/2uA==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/parser@8.0.6': + resolution: {integrity: sha512-LpGDIYJAzc3Y3PT9x+FxBrGYu2PlWxLmyN7SwPTzaX0LSwEmFJBsRnPBqmaXF2r1v+Zhz6lNiEJ+7yKtM5+Ugg==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@babel/template@8.0.0': + resolution: {integrity: sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/traverse@8.0.6': + resolution: {integrity: sha512-ilYV+qLStGzSKLipJlETdvdtpK3rAuwfCmCoGKv15MU4EoeIuLkespSNoNLFGOPSciEHx9TLWtN5mv0fz5w5mQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/types@8.0.6': + resolution: {integrity: sha512-c+xgSWboV2pdwXP67BUWDVRHi2BO5Q3KwxyFVs7taVTY+fsK47/L51ZXQoV9rozHJDNHvJ4v4WEsI/IlxV2l2A==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@csstools/color-helpers@6.1.1': + resolution: {integrity: sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.4.0': + resolution: {integrity: sha512-XQKj5B7QiZcHiegCOCAzcAOJdhGgWOHbbu62h5e5mkHnn8lWcfiJhllkqWmxu5zWR9jucPHuo1iTB56P033hcg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.2.3': + resolution: {integrity: sha512-y4LpL+lmpuyKDiEFq2PnZUVFdAjsoB/qQJod79yLNokXyW7jewi+/WJ69EfItj8A2unWtxXnGjw6LYXgXu5ZjA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.14': + resolution: {integrity: sha512-HpbVXyrofRXpHpgkNIjU/3EWR4WJvOkO3emNK/L6X/mTJU7bGUI3AkkpoTNXznQLp0KRjLHELTGeKI5dIkI9JQ==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.1': + resolution: {integrity: sha512-bPlN9S9O1A0euCpEWE4qnvB5YDuyYVsUTrxSgmAM1Is0j4tICHoVyOVAXfWMP/kS9ZrjvyIXWV2PmomiAXXqOw==} + engines: {node: '>=20.19.0'} + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.28.2': + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.28.2': + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.28.2': + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.28.2': + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.28.2': + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.2': + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.28.2': + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.2': + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.28.2': + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.28.2': + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.28.2': + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.28.2': + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.28.2': + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.28.2': + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.2': + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.28.2': + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.28.2': + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-arm64@0.28.2': + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.2': + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-arm64@0.28.2': + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.2': + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/openharmony-arm64@0.28.2': + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.28.2': + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.28.2': + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.28.2': + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.28.2': + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@exodus/bytes@1.15.1': + resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + '@noble/hashes': ^1.8.0 || ^2.0.0 + peerDependenciesMeta: + '@noble/hashes': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/gen-mapping@0.4.0-beta.0': + resolution: {integrity: sha512-JdGNkbE4GlNPYQhM0L95fBQr7ctLZJ276QXQLTad4t1oSdnnCI3fDq9DW3BqYAWv8Wc3+HS+4Gsii1oPMCfz1w==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/nice-android-arm-eabi@1.1.1': + resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@napi-rs/nice-android-arm64@1.1.1': + resolution: {integrity: sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/nice-darwin-arm64@1.1.1': + resolution: {integrity: sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/nice-darwin-x64@1.1.1': + resolution: {integrity: sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/nice-freebsd-x64@1.1.1': + resolution: {integrity: sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/nice-linux-arm-gnueabihf@1.1.1': + resolution: {integrity: sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/nice-linux-arm64-gnu@1.1.1': + resolution: {integrity: sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/nice-linux-arm64-musl@1.1.1': + resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/nice-linux-ppc64-gnu@1.1.1': + resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==} + engines: {node: '>= 10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@napi-rs/nice-linux-riscv64-gnu@1.1.1': + resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@napi-rs/nice-linux-s390x-gnu@1.1.1': + resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==} + engines: {node: '>= 10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@napi-rs/nice-linux-x64-gnu@1.1.1': + resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/nice-linux-x64-musl@1.1.1': + resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/nice-openharmony-arm64@1.1.1': + resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [openharmony] + + '@napi-rs/nice-win32-arm64-msvc@1.1.1': + resolution: {integrity: sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/nice-win32-ia32-msvc@1.1.1': + resolution: {integrity: sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@napi-rs/nice-win32-x64-msvc@1.1.1': + resolution: {integrity: sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/nice@1.1.1': + resolution: {integrity: sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==} + engines: {node: '>= 10'} + + '@oxc-project/types@0.150.0': + resolution: {integrity: sha512-rDS5/31E9HfPl/CIzGrn0DOlvBbXFseQ5URJ9sYMfstbKLD/c6Gm9vmRzRGDdAXyOIL4zmO37lc9RIwYqVruZw==} + + '@parcel/watcher-android-arm64@2.6.0': + resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.6.0': + resolution: {integrity: sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.6.0': + resolution: {integrity: sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.6.0': + resolution: {integrity: sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.6.0': + resolution: {integrity: sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.6.0': + resolution: {integrity: sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + resolution: {integrity: sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.6.0': + resolution: {integrity: sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.6.0': + resolution: {integrity: sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.6.0': + resolution: {integrity: sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.6.0': + resolution: {integrity: sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-x64@2.6.0': + resolution: {integrity: sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.6.0': + resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==} + engines: {node: '>= 10.0.0'} + + '@rolldown/binding-android-arm-eabi@1.2.9': + resolution: {integrity: sha512-tNISae1QEf/vkb3xkRcjV5SEdzPE97We5IVaa2Z8jSszQPZ8U60B/YCYpw4QI7VidYsBtKavczXf+DyDs9WGxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.9': + resolution: {integrity: sha512-YC8YsI30o606GTZi0VyzYlsDKFP8W61i/QzayHDkLbNEz/IShqAmTa+hsJRj13xTHA0H+6fk4b2UmGn+Q/cMlg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.2.9': + resolution: {integrity: sha512-IwhlH3qK5urrY8hZiEgGkHKEFN901p/p2bjxCxJlr4GyNnF7wYpUvK+Y43uaRYuC4hpfjzbR3SJC3arX1jGvmw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.2.9': + resolution: {integrity: sha512-XxpJfVzFh+jilRxIXUqcfYAYcunIc/XEzIizsOL1fcJee5Sf7H3mH8WlLmfHfluz5amqR88QQo9izKtmMlavAw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.2.9': + resolution: {integrity: sha512-kSfvhmgeWyfkbT3p/1s5vSgboogoah2zkm9fX2zjg2hHxSV7T4KhMWRUUaRk4OXNqoD3QAUeRqLcs1aZOK4U1g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.2.9': + resolution: {integrity: sha512-1RVzG17pxqbTfYLC352JlLt6kKLG+6Hr30n8DlIJqsnV5luUDd2Qdx9Ayw1Cabfyb1K9k0jXEZ7evxkRoT+uiw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.2.9': + resolution: {integrity: sha512-BXqPvZ2drqVD+/Z8UpKwcs4Mp7grM+eGFku4CAEKrEtcbAsUpzREphK1sogCRZGreVPiMkiiBtw0n3TPteuqvw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.2.9': + resolution: {integrity: sha512-11vWvo8YDwLzukt27J3aYDWU+gg2P7J+ZOmiJ0hkF5BXZDW7pVya7r40MXDy6ya0i9KamoENSVKIugvJNgFXIA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.2.9': + resolution: {integrity: sha512-a1tijMkdwsIARtc0F39ApURROkf3NwqinI6TOiSSWCTR7dT96dffNvMUtDHnq64wKNTIZOIlzKrFvvFUznJiyw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.2.9': + resolution: {integrity: sha512-x6SQNdAvv4c3hWqTMaWuawzMX9myaCs/yEmlGsxJzkdClnHW7FbrjQuSiRDhuSYzEYoEMhsaJy9qHG/XNemJPQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.2.9': + resolution: {integrity: sha512-9s0AZ8BFK5/n7B/TBoa2yJE3gI3KURrbXcPBlsAsvjU4VeJKgE90y1YtNxyEUIcHPQkg6/yfF3qihUrcM/Kf0Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.2.9': + resolution: {integrity: sha512-P7VWAmV+WdJluH7ovnRGoiv2i8To7GAZ+kGzfGup635cyL7SyYl3lSUaA3Gp5THf0n/Co5EyEqb2zbqq+nMOHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.2.9': + resolution: {integrity: sha512-1qixtsE4BK8h+yS3BfmZ09UhA7O/N4IACva6YBr7EBvCJraByTuRcgOTaiA62Tm0vey3UcKXLOaoGHtYmNGEVg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-win32-arm64-msvc@1.2.9': + resolution: {integrity: sha512-ok8IQjcEPs1AKZfuEUznVBrJw+gK4soq+bx8b1X2XoMqVClarc1q5JDmVtWXY1xfr6ZuHTAsPXHTgTrqKTZeww==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.2.9': + resolution: {integrity: sha512-Ip2mXoU0hM0boq3Rf+ekuT653OROSo6aSYcPT1VHE4q52KvyxgFkQgrgb/IEsxOuvQ2fZZbs8khJAyCEPM24/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.4.0': + resolution: {integrity: sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.63.4': + resolution: {integrity: sha512-I+BSHzTAhKN2n7ZwGZsegGcZjDpLqFOMAtJz/u6uFGe0pUFbq56dEHjqJV/ZUdRJtNXNxA+hREUatZBvMR3Oiw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.63.4': + resolution: {integrity: sha512-pu3BdjS2LtEzRu2elmGzS3fIeWSZy4BMDIaLNwjorO76+k2d0LMluijhsDx3KQyQBQ/lLUZCQA9/s6csvUfuhw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.63.4': + resolution: {integrity: sha512-xfSrj9MHnWK9GaSqT9U0ImHtH/N8WZlHLx4cZHiuLcqs640hvZ3hLPd5UR2AZS57FaE8HrRUSpltbZdWRxHiDA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.63.4': + resolution: {integrity: sha512-bqU99PLJb/dqb3S0GIMdeuyAEETSUgZBoqXYd3Sd+WCsV+MmPhnN6JrotWyir31+QgH7EvvE5/mwGJlEoci8Fw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.63.4': + resolution: {integrity: sha512-JinsFZ5G40oXQb+sUuiA5x689vhr6dDYK0H0NL+rwKdL6CqnmYN8PE4ZwfRSoIjrCxqTQG/SLfTtSvHeGxoVlw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.63.4': + resolution: {integrity: sha512-GAdA4UxpiNm27cLHr2GqXBpAD0x9FqwYBY7/YSP0Ss0/PNi4k8gbviqpIpYbVSRBaS2ZcegXEzgTQMbRNCwxCw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + resolution: {integrity: sha512-qDd6NoA1znaLjp4jR5U/KWCdLAKDJNB8W9ChbbDaKbo0xA+Atln5HK6LFCZ4oJQpemtRZA288DCirFRjrspptw==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + resolution: {integrity: sha512-WtB5Tz5KTNINb8ZA+8sQ7bmjuS1JrRT7YverYIhUGdWWDlpzVWmIwuZE+jidkEXUn1l0zrEkaIMa8dHF3NGcsA==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.63.4': + resolution: {integrity: sha512-VcQ3L1tjnkKzWjryAVaFhHEWcqOfICX9uxVVoDzm2t0DpgKRHd2zOpVrJc0xsWeBZcBFyYROCIBdyR/fS174pg==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.63.4': + resolution: {integrity: sha512-6+ZQX6P5s0cMDN2Ypb8Lbm2+/sZYmZjdaYny992ujUU9UKi/4CWoJWsl1pNvjWJHNHGK51m+jKGLlh1ylb2ifQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.63.4': + resolution: {integrity: sha512-D72ZnvkFkBXOfzMMQLcwfPLyGkKb7HZ9/mf97B7v6/P5Lbv4oFOtSY/uHbS8lH6uKUOxoKiuokdb50XZSzzbJw==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.63.4': + resolution: {integrity: sha512-piU6BxeqA3O9KSu3kRCIQQtNqFFaTu21SEV4FwaRZowpnj3bLaWPZHw+xFqCs0XlJ+aOH3PTRWGoglH+mKA/OA==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + resolution: {integrity: sha512-/5PGpHwqt2EEEOUs1XwzubE/ucr0dWDQ+to3zqi4Ds7EWpwtQ79wXc4JBoxqj/OwpawTsKWzJxHfSuBOq3DrWA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.63.4': + resolution: {integrity: sha512-cX3beZDLWt7G2oJF+nhChiT+qtaihs+S2xi7ziGmVB+2pwPng6D0Ed0HmElQOgv2UsUmSJJLGwpBao/3TDx3VA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + resolution: {integrity: sha512-1uz2mGWHyptR7DgHHrlbdRAjXK7v7elGZ9lMja910/RP+ZYbX6xAmCiU9UZSX4hqmgtHMv6lr5l3kq1HIOpcag==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.63.4': + resolution: {integrity: sha512-nLS8topojxyz7SRpKR2IODRpQ0XPZ+xaOXvT3+hqK/Uy8Lo5HFgkkIBiIrCu5tL5YqzTvgovGw55PwpahTAGig==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.63.4': + resolution: {integrity: sha512-gs7DRKotr3l3q+jGPQBjH0ng1FjlEDm5ueQrkw5JtQvtLyEIcLASqAEaor56BhkKRzk+IcQzrcanBdb/bBQn8g==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.63.4': + resolution: {integrity: sha512-791ET7W17NnScOZM7h4dX5hYspxE28htPFsb1awY/NRR8+PRNkS53e475rDdxXXDrP+kwnCcNWg9CX5ztn/Aqw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.63.4': + resolution: {integrity: sha512-iwZQRcmj7g88g3tzefIrQY7qvmuA/cfYwhrDtTBhsmukO4U2huVO5W+86XacUMRvdSFVAc6kZUZy21JaRwiB9w==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.63.4': + resolution: {integrity: sha512-dVHFp9gRWrdTpnqQuGfCwd7hOQDatK1VCP2iWhLY/cGrOQs/ucFzJ6A5SRqbXX12ZDI8EUuejSM5kwg+ja7Png==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.63.4': + resolution: {integrity: sha512-t3NlauOW6gxZVVFcBEnO62Cb4wbyDFL416gTg1uFI/2tgqYQlf69FbSE115Ajre9I+c26Lk4mcmdFUsS/DGifQ==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.63.4': + resolution: {integrity: sha512-xWuIaSye5FWZF8+UYtVEcHtRJDN5kN9Kfgxx3Kq8XIov9KSKbc1fiqQCm90SKrgQbUXZelbnUhnlUJmfSE7P9A==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.63.4': + resolution: {integrity: sha512-9ALJJUOg/ZflMJepVo2PlgsGxSaxN7SQ4Z8GoZfVlarWr6r3rkHUNsd/zAio7p4YMtChSMXPionxej4Hkf6CXQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.63.4': + resolution: {integrity: sha512-blj9z5qx/Pv4WU0W1NMFDB97e0JH5ed+aZGywW8WCvp/NhWX/4PFAq5uu6Q0AebNn+Vo6KzUYDT++JzTT5ojlQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.63.4': + resolution: {integrity: sha512-Erx822VRBwLa124shbj+wNXe//BOgMEctDV0m1aqTQdNO1S69DgNUCFKC1RCeZfixs1J31l6igk1ziyXErbigQ==} + cpu: [x64] + os: [win32] + + '@rollup/wasm-node@4.63.4': + resolution: {integrity: sha512-pXmzGQzpMazI9wL+5ifqPfIOR5FFblTWTPS/tIg/WXH8JpLYFbJsOq4UyL2RH2FNAnfPqoC28wgRLudGH66NPA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@testing-library/dom@10.4.2': + resolution: {integrity: sha512-yzr2S9HyAIdhz2/6qHgbs665Q7PKVcDF05vsOlHPxG1mo36gKVesdYVeDLnXgfjJ03CrKRk08knc6+E/9m8v2Q==} + engines: {node: '>=18'} + + '@testing-library/react@16.3.3': + resolution: {integrity: sha512-Uo193NgQbPMz6lrrhtRQQFcMC6Re/ELLFbbuVL30WDlZxlpZf9/lMHTAVxPRLw1q1iu9OJmR1c2BLiENRstdBg==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/gensync@1.0.5': + resolution: {integrity: sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==} + + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + + '@types/react@19.3.0': + resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + + '@types/use-sync-external-store@1.7.0': + resolution: {integrity: sha512-v8435kzWm3FuxyUa4r8o4sG6b0D+r+uOrzF5MXHxZKW4Haoo6txu/lq56Vio45e2i1nkmFPA+R7myWcmV7Z7Bg==} + + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + baseline-browser-mapping@2.11.25: + resolution: {integrity: sha512-gMmEShwwq7FJqMwvfRwvCl00v4kN+KOfJqXn+f4nrufak5gNHJOksd/60Dvjuz7sI8Y5WiSFBa8FEYr+zoyqCw==} + engines: {node: '>=6.0.0'} + hasBin: true + + bidi-js@1.1.0: + resolution: {integrity: sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg==} + + browserslist@4.29.0: + resolution: {integrity: sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + cli-cursor@5.0.0: + resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==} + engines: {node: '>=18'} + + cli-spinners@3.4.0: + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} + engines: {node: '>=18.20'} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + commander@15.0.0: + resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} + engines: {node: '>=22.12.0'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + common-path-prefix@3.0.0: + resolution: {integrity: sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + cssstyle@5.3.7: + resolution: {integrity: sha512-7D2EPVltRrsTkhpQmksIu+LxeWAIEk6wRDMJ1qljlv+CKHJM+cJLlfhWIzNA44eAsHXSNe3+vO6DW1yCYx8SuQ==} + engines: {node: '>=20'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@6.0.1: + resolution: {integrity: sha512-euIQENZg6x8mj3fO6o9+fOW8MimUI4PpD/fZBhJfeioZVy9TUpM4UY7KjQNVZFlqwJ0UdzRDzkycB997HEq1BQ==} + engines: {node: '>=20'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@3.2.7: + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + dependency-graph@1.0.0: + resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==} + engines: {node: '>=4'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + electron-to-chromium@1.5.433: + resolution: {integrity: sha512-5lCAbyZBjtmUt/RAGHRqrL2q0oEFRThDAsZHHDn9XHa89Qw7gMYOeSicBTy+AHfvo0r6vwsZvqNJTQIQy1BLzA==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + empathic@2.1.0: + resolution: {integrity: sha512-AnfC1ATldl49/cvZdLPDjBfrRNwbDO05aibiOtzQu3qtlbJtomNLhF30HEtn/7iBz50dlMECqATo3fG0LrdEgw==} + engines: {node: '>=14'} + + entities@8.1.0: + resolution: {integrity: sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==} + engines: {node: '>=20.19.0'} + + errno@0.1.8: + resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==} + hasBin: true + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + esbuild@0.28.2: + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.8: + resolution: {integrity: sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + find-cache-directory@6.0.0: + resolution: {integrity: sha512-CvFd5ivA6HcSHbD+59P7CyzINHXzwhuQK8RY7CxJZtgDSAtRlHiCaQpZQ2lMR/WRyUIEmzUvL6G2AGurMfegZA==} + engines: {node: '>=20'} + + find-up-simple@1.0.1: + resolution: {integrity: sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==} + engines: {node: '>=18'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + flru@1.0.2: + resolution: {integrity: sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==} + engines: {node: '>=6'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.7.0: + resolution: {integrity: sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q==} + engines: {node: '>=18'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + immutable@5.1.9: + resolution: {integrity: sha512-m8nVez3rwrgmWxtLMt1ZYXB2Lv7OKYn/disyxAlSDYAlKSlFoPPfIAmAM/M5xqL4m4C/wAPw7S2/CNaUii1Hxg==} + + import-meta-resolve@4.2.0: + resolution: {integrity: sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==} + + injection-js@2.6.1: + resolution: {integrity: sha512-dbR5bdhi7TWDoCye9cByZqeg/gAfamm8Vu3G1KZOTYkOif8WkuM8CD0oeDPtZYMzT5YH76JAFB7bkmyY9OJi2A==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@2.0.0: + resolution: {integrity: sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==} + engines: {node: '>=12'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-tokens@10.0.0: + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@27.4.0: + resolution: {integrity: sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + less@4.9.1: + resolution: {integrity: sha512-orp15PfJvvNDIqJdVWzMI9Sjpjp3VTiw3sfvbB+67LlISTEn8uVT2EdYSuyl02BLvaftv6sdk9Umnxmm5rckmg==} + engines: {node: '>=18'} + hasBin: true + + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + + lru-cache@11.5.3: + resolution: {integrity: sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==} + engines: {node: 20 || >=22} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + make-dir@5.1.0: + resolution: {integrity: sha512-IfpFq6UM39dUNiphpA6uDezNx/AvWyhwfICWPR3t1VspkgkMZrL+Rk1RbN1bx+aeNYwOrqGJgEgV3yotk+ZUVw==} + engines: {node: '>=18'} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-function@5.0.1: + resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==} + engines: {node: '>=18'} + + mlly@1.8.2: + resolution: {integrity: sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==} + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.19: + resolution: {integrity: sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + needle@2.9.1: + resolution: {integrity: sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==} + engines: {node: '>= 4.4.x'} + hasBin: true + + needle@3.5.0: + resolution: {integrity: sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w==} + engines: {node: '>= 4.4.x'} + hasBin: true + + ng-packagr@22.1.1: + resolution: {integrity: sha512-jZzpQckw2SFvuYI3aWfYMefSVKKG/04E+vrX2KD6coMx7ciLBotVWuLc0331Fjb6XNmAVcsmgQ5NY/Lf+9Himw==} + engines: {node: ^22.22.3 || ^24.15.0 || >=26.0.0} + hasBin: true + peerDependencies: + '@angular/compiler-cli': ^22.0.0 || ^22.1.0-next || ^22.2.0-next + tailwindcss: ^2.0.0 || ^3.0.0 || ^4.0.0 + tslib: ^2.3.0 + typescript: '>=6.0 <6.1' + peerDependenciesMeta: + tailwindcss: + optional: true + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-releases@2.0.56: + resolution: {integrity: sha512-x0InOIyzgdk+eyaWaRJFH5snEtiImgBgblZ2CyPrLmqqcuMQkEvcDPHbzqbD8eDsSeJbVOjn+crzyzHaM4D+/A==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + obug@2.2.1: + resolution: {integrity: sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==} + engines: {node: '>=12.20.0'} + + onetime@7.0.0: + resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} + engines: {node: '>=18'} + + ora@9.4.1: + resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==} + engines: {node: '>=20'} + + parse-node-version@1.0.1: + resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} + engines: {node: '>= 0.10'} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + piscina@5.3.2: + resolution: {integrity: sha512-vv7l/mM7B9WgrhDaudIqv1x6v4LOBRYYmpWGdWushhpbCmVHp11sUB2v4hj1CAeTGXVxuZFz6xOU1RalAiPKfQ==} + engines: {node: '>=20.x'} + + pkg-dir@8.0.0: + resolution: {integrity: sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==} + engines: {node: '>=18'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + yaml: + optional: true + + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} + engines: {node: ^10 || ^12 || >=14} + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + probe-image-size@7.4.0: + resolution: {integrity: sha512-cdEprVtZxV+awMde9X+4jILBFYh4CARxVrQaMl4wY4YcPWbul9jntXrIW95NInBDyJwcVUP3U0T6yukN8rMBaQ==} + + prr@1.0.1: + resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-dom@19.3.0: + resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==} + peerDependencies: + react: ^19.3.0 + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react@19.3.0: + resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + readdirp@5.1.1: + resolution: {integrity: sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==} + engines: {node: '>= 20.19.0'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + restore-cursor@5.1.0: + resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==} + engines: {node: '>=18'} + + rolldown@1.2.9: + resolution: {integrity: sha512-hx/Pv0N1haXRb11qkfnK5MXB/iqr7i0yjWQqmO9uHqZpBgQSqzc8UsSnEpalsh+j1I8qQ2CkXAkJC8Br3dKSlg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup-plugin-dts@6.4.1: + resolution: {integrity: sha512-l//F3Zf7ID5GoOfLfD8kroBjQKEKpy1qfhtAdnpibFZMffPaylrg1CoDC2vGkPeTeyxUe4bVFCln2EFuL7IGGg==} + engines: {node: '>=20'} + peerDependencies: + rollup: ^3.29.4 || ^4 + typescript: ^4.5 || ^5.0 || ^6.0 + + rollup@4.63.4: + resolution: {integrity: sha512-4U0liVayNIoLp3GFl1FcI8561WepLnZ1rqfraGh7S9B3Ur5F9S283y8Futii7RUU2C/97tOBmBy7nYvhoiOpbQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sass@1.104.1: + resolution: {integrity: sha512-yDA+1aIG3EHgN4V/BvuhCvu61FF6hEd4e+9DxikUm9U0CAGuvdIZ/UYy7qbOxjhbbWQraoyLlMuzdRGOSV5Bmw==} + engines: {node: '>=20.19.0'} + hasBin: true + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.28.0: + resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + + stdin-discarder@0.3.2: + resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} + engines: {node: '>=18'} + + stream-parser@0.3.1: + resolution: {integrity: sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + string-width@8.2.2: + resolution: {integrity: sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==} + engines: {node: '>=20'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyexec@1.3.1: + resolution: {integrity: sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.4.13: + resolution: {integrity: sha512-mbYsrih5FRtGxs3Usvl/PqwJsNpp+jsmrdFviiK02teHDG0/HebBG/pqCylje3kzgXYzuLoHJF/0mz9W53t8Xg==} + + tldts@7.4.13: + resolution: {integrity: sha512-iHtaIWWIbMDkCeJdTBzZFGgbluE5J+oHlb2g7+oAz1S1gpuVpabRZdQyd471Vl8UUkcz2vXSL8xZH2kyCe8tfA==} + hasBin: true + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + '@microsoft/api-extractor': ^7.36.0 + '@swc/core': ^1 + postcss: ^8.4.12 + typescript: '>=4.5.0' + peerDependenciesMeta: + '@microsoft/api-extractor': + optional: true + '@swc/core': + optional: true + postcss: + optional: true + typescript: + optional: true + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.4: + resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==} + + update-browserslist-db@1.3.3: + resolution: {integrity: sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + use-sync-external-store@1.7.0: + resolution: {integrity: sha512-6L+EeigHMQhdaIPNIFUKwfWJSwWFQ8gJbJ2DLOs5sDIegTwR9fRxvnM3uciHKjIZhFz+KAv2emhWMRvDmMcY8A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + verkit@0.3.2: + resolution: {integrity: sha512-zj/ob3UsvJGN0whEAKFp53REA5X66hvffVqoCtVQAakJKnKlH+/PcOfMoFwIG/o4rElqLv/ycAFlx8ZlXUorCg==} + engines: {node: '>=18.12.0'} + + vite@8.3.0: + resolution: {integrity: sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.7.1 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@15.1.0: + resolution: {integrity: sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==} + engines: {node: '>=20'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.1.0: + resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yoctocolors@2.2.0: + resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} + engines: {node: '>=18'} + +snapshots: + + '@acemir/cssom@0.9.31': {} + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@angular/core': 22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2) + rxjs: 7.8.2 + tslib: 2.8.1 + + '@angular/compiler-cli@22.1.7(@angular/compiler@22.1.7)(typescript@6.0.3)': + dependencies: + '@angular/compiler': 22.1.7 + '@babel/core': 8.0.1 + '@jridgewell/sourcemap-codec': 1.6.0 + chokidar: 5.0.0 + convert-source-map: 1.9.0 + reflect-metadata: 0.2.2 + semver: 7.8.5 + tslib: 2.8.1 + yargs: 18.1.0 + optionalDependencies: + typescript: 6.0.3 + + '@angular/compiler@22.1.7': + dependencies: + tslib: 2.8.1 + + '@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2)': + dependencies: + rxjs: 7.8.2 + tslib: 2.8.1 + optionalDependencies: + '@angular/compiler': 22.1.7 + + '@angular/platform-browser@22.1.7(@angular/common@22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))': + dependencies: + '@angular/common': 22.1.7(@angular/core@22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2))(rxjs@7.8.2) + '@angular/core': 22.1.7(@angular/compiler@22.1.7)(rxjs@7.8.2) + tslib: 2.8.1 + + '@asamuzakjp/css-color@4.1.2': + dependencies: + '@csstools/css-calc': 3.4.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.1))(@csstools/css-tokenizer@4.0.1) + '@csstools/css-color-parser': 4.2.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.1))(@csstools/css-tokenizer@4.0.1) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.1) + '@csstools/css-tokenizer': 4.0.1 + lru-cache: 11.5.3 + + '@asamuzakjp/dom-selector@6.8.1': + dependencies: + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.1.0 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.5.3 + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/code-frame@8.0.6': + dependencies: + '@babel/helper-validator-identifier': 8.0.6 + js-tokens: 10.0.0 + + '@babel/compat-data@8.0.5': {} + + '@babel/core@8.0.1': + dependencies: + '@babel/code-frame': 8.0.6 + '@babel/generator': 8.0.6 + '@babel/helper-compilation-targets': 8.0.6 + '@babel/helpers': 8.0.5 + '@babel/parser': 8.0.6 + '@babel/template': 8.0.0 + '@babel/traverse': 8.0.6 + '@babel/types': 8.0.6 + '@types/gensync': 1.0.5 + convert-source-map: 2.0.0 + empathic: 2.1.0 + gensync: 1.0.0-beta.2 + import-meta-resolve: 4.2.0 + json5: 2.2.3 + obug: 2.2.1 + semver: 7.8.5 + + '@babel/generator@8.0.6': + dependencies: + '@babel/parser': 8.0.6 + '@babel/types': 8.0.6 + '@jridgewell/gen-mapping': 0.4.0-beta.0 + '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@8.0.6': + dependencies: + '@babel/compat-data': 8.0.5 + '@babel/helper-validator-option': 8.0.0 + browserslist: 4.29.0 + flru: 1.0.2 + verkit: 0.3.2 + + '@babel/helper-globals@8.0.6': {} + + '@babel/helper-string-parser@8.0.6': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-identifier@8.0.6': {} + + '@babel/helper-validator-option@8.0.0': {} + + '@babel/helpers@8.0.5': + dependencies: + '@babel/template': 8.0.0 + '@babel/types': 8.0.6 + + '@babel/parser@8.0.6': + dependencies: + '@babel/types': 8.0.6 + + '@babel/runtime@7.29.7': {} + + '@babel/template@8.0.0': + dependencies: + '@babel/code-frame': 8.0.6 + '@babel/parser': 8.0.6 + '@babel/types': 8.0.6 + + '@babel/traverse@8.0.6': + dependencies: + '@babel/code-frame': 8.0.6 + '@babel/generator': 8.0.6 + '@babel/helper-globals': 8.0.6 + '@babel/parser': 8.0.6 + '@babel/template': 8.0.0 + '@babel/types': 8.0.6 + obug: 2.2.1 + + '@babel/types@8.0.6': + dependencies: + '@babel/helper-string-parser': 8.0.6 + '@babel/helper-validator-identifier': 8.0.6 + + '@csstools/color-helpers@6.1.1': {} + + '@csstools/css-calc@3.4.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.1))(@csstools/css-tokenizer@4.0.1)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.1) + '@csstools/css-tokenizer': 4.0.1 + + '@csstools/css-color-parser@4.2.3(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.1))(@csstools/css-tokenizer@4.0.1)': + dependencies: + '@csstools/color-helpers': 6.1.1 + '@csstools/css-calc': 3.4.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.1))(@csstools/css-tokenizer@4.0.1) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.1) + '@csstools/css-tokenizer': 4.0.1 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.1)': + dependencies: + '@csstools/css-tokenizer': 4.0.1 + + '@csstools/css-syntax-patches-for-csstree@1.1.14(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.1': {} + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/aix-ppc64@0.28.2': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.28.2': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-arm@0.28.2': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/android-x64@0.28.2': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.28.2': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.28.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.28.2': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.28.2': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.28.2': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-arm@0.28.2': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.28.2': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.28.2': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.28.2': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.28.2': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.28.2': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.28.2': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/linux-x64@0.28.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.28.2': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.28.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.28.2': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.28.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.28.2': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.28.2': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.28.2': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.28.2': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@esbuild/win32-x64@0.28.2': + optional: true + + '@exodus/bytes@1.15.1': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/gen-mapping@0.4.0-beta.0': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + optional: true + + '@napi-rs/nice-android-arm-eabi@1.1.1': + optional: true + + '@napi-rs/nice-android-arm64@1.1.1': + optional: true + + '@napi-rs/nice-darwin-arm64@1.1.1': + optional: true + + '@napi-rs/nice-darwin-x64@1.1.1': + optional: true + + '@napi-rs/nice-freebsd-x64@1.1.1': + optional: true + + '@napi-rs/nice-linux-arm-gnueabihf@1.1.1': + optional: true + + '@napi-rs/nice-linux-arm64-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-arm64-musl@1.1.1': + optional: true + + '@napi-rs/nice-linux-ppc64-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-riscv64-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-s390x-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-x64-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-x64-musl@1.1.1': + optional: true + + '@napi-rs/nice-openharmony-arm64@1.1.1': + optional: true + + '@napi-rs/nice-win32-arm64-msvc@1.1.1': + optional: true + + '@napi-rs/nice-win32-ia32-msvc@1.1.1': + optional: true + + '@napi-rs/nice-win32-x64-msvc@1.1.1': + optional: true + + '@napi-rs/nice@1.1.1': + optionalDependencies: + '@napi-rs/nice-android-arm-eabi': 1.1.1 + '@napi-rs/nice-android-arm64': 1.1.1 + '@napi-rs/nice-darwin-arm64': 1.1.1 + '@napi-rs/nice-darwin-x64': 1.1.1 + '@napi-rs/nice-freebsd-x64': 1.1.1 + '@napi-rs/nice-linux-arm-gnueabihf': 1.1.1 + '@napi-rs/nice-linux-arm64-gnu': 1.1.1 + '@napi-rs/nice-linux-arm64-musl': 1.1.1 + '@napi-rs/nice-linux-ppc64-gnu': 1.1.1 + '@napi-rs/nice-linux-riscv64-gnu': 1.1.1 + '@napi-rs/nice-linux-s390x-gnu': 1.1.1 + '@napi-rs/nice-linux-x64-gnu': 1.1.1 + '@napi-rs/nice-linux-x64-musl': 1.1.1 + '@napi-rs/nice-openharmony-arm64': 1.1.1 + '@napi-rs/nice-win32-arm64-msvc': 1.1.1 + '@napi-rs/nice-win32-ia32-msvc': 1.1.1 + '@napi-rs/nice-win32-x64-msvc': 1.1.1 + optional: true + + '@oxc-project/types@0.150.0': {} + + '@parcel/watcher-android-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-x64@2.6.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.6.0': + optional: true + + '@parcel/watcher-win32-arm64@2.6.0': + optional: true + + '@parcel/watcher-win32-x64@2.6.0': + optional: true + + '@parcel/watcher@2.6.0': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.7 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.6.0 + '@parcel/watcher-darwin-arm64': 2.6.0 + '@parcel/watcher-darwin-x64': 2.6.0 + '@parcel/watcher-freebsd-x64': 2.6.0 + '@parcel/watcher-linux-arm-glibc': 2.6.0 + '@parcel/watcher-linux-arm-musl': 2.6.0 + '@parcel/watcher-linux-arm64-glibc': 2.6.0 + '@parcel/watcher-linux-arm64-musl': 2.6.0 + '@parcel/watcher-linux-x64-glibc': 2.6.0 + '@parcel/watcher-linux-x64-musl': 2.6.0 + '@parcel/watcher-win32-arm64': 2.6.0 + '@parcel/watcher-win32-x64': 2.6.0 + optional: true + + '@rolldown/binding-android-arm-eabi@1.2.9': + optional: true + + '@rolldown/binding-android-arm64@1.2.9': + optional: true + + '@rolldown/binding-darwin-arm64@1.2.9': + optional: true + + '@rolldown/binding-darwin-x64@1.2.9': + optional: true + + '@rolldown/binding-freebsd-x64@1.2.9': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.2.9': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.2.9': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.2.9': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.2.9': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.2.9': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.2.9': + optional: true + + '@rolldown/binding-linux-x64-musl@1.2.9': + optional: true + + '@rolldown/binding-openharmony-arm64@1.2.9': + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.2.9': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.9': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + + '@rollup/plugin-json@6.1.0(rollup@4.63.4)': + dependencies: + '@rollup/pluginutils': 5.4.0(rollup@4.63.4) + optionalDependencies: + rollup: 4.63.4 + + '@rollup/pluginutils@5.4.0(rollup@4.63.4)': + dependencies: + '@types/estree': 1.0.9 + estree-walker: 2.0.2 + picomatch: 4.0.7 + optionalDependencies: + rollup: 4.63.4 + + '@rollup/rollup-android-arm-eabi@4.63.4': + optional: true + + '@rollup/rollup-android-arm64@4.63.4': + optional: true + + '@rollup/rollup-darwin-arm64@4.63.4': + optional: true + + '@rollup/rollup-darwin-x64@4.63.4': + optional: true + + '@rollup/rollup-freebsd-arm64@4.63.4': + optional: true + + '@rollup/rollup-freebsd-x64@4.63.4': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.63.4': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.63.4': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.63.4': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.63.4': + optional: true + + '@rollup/rollup-linux-x64-musl@4.63.4': + optional: true + + '@rollup/rollup-openbsd-x64@4.63.4': + optional: true + + '@rollup/rollup-openharmony-arm64@4.63.4': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.63.4': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.63.4': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.63.4': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.63.4': + optional: true + + '@rollup/wasm-node@4.63.4': + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + fsevents: 2.3.3 + + '@standard-schema/spec@1.1.0': {} + + '@testing-library/dom@10.4.2': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/runtime': 7.29.7 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/react@16.3.3(@testing-library/dom@10.4.2)(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)': + dependencies: + '@babel/runtime': 7.29.7 + '@testing-library/dom': 10.4.2 + react: 19.3.0 + react-dom: 19.3.0(react@19.3.0) + optionalDependencies: + '@types/react': 19.3.0 + + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.9': {} + + '@types/gensync@1.0.5': {} + + '@types/jsesc@2.5.1': {} + + '@types/react@19.3.0': + dependencies: + csstype: 3.2.3 + + '@types/use-sync-external-store@1.7.0': {} + + '@vitest/expect@4.1.11': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + + '@vitest/mocker@4.1.11(vite@8.3.0(esbuild@0.27.7)(less@4.9.1)(sass@1.104.1))': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.3.0(esbuild@0.27.7)(less@4.9.1)(sass@1.104.1) + + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.1.1 + + '@vitest/runner@4.1.11': + dependencies: + '@vitest/utils': 4.1.11 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.11': {} + + '@vitest/utils@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + + acorn@8.18.0: {} + + agent-base@7.1.4: {} + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.8 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: {} + + ansi-regex@6.3.0: {} + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + any-promise@1.3.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + assertion-error@2.0.1: {} + + baseline-browser-mapping@2.11.25: {} + + bidi-js@1.1.0: + dependencies: + require-from-string: 2.0.2 + + browserslist@4.29.0: + dependencies: + baseline-browser-mapping: 2.11.25 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.433 + node-releases: 2.0.56 + update-browserslist-db: 1.3.3(browserslist@4.29.0) + + bundle-require@5.1.0(esbuild@0.27.7): + dependencies: + esbuild: 0.27.7 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + caniuse-lite@1.0.30001810: {} + + chai@6.2.2: {} + + chalk@5.6.2: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chokidar@5.0.0: + dependencies: + readdirp: 5.1.1 + + cli-cursor@5.0.0: + dependencies: + restore-cursor: 5.1.0 + + cli-spinners@3.4.0: {} + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + commander@15.0.0: {} + + commander@4.1.1: {} + + common-path-prefix@3.0.0: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + cssstyle@5.3.7: + dependencies: + '@asamuzakjp/css-color': 4.1.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.14(css-tree@3.2.1) + css-tree: 3.2.1 + lru-cache: 11.5.3 + + csstype@3.2.3: {} + + data-urls@6.0.1: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 15.1.0 + + debug@2.6.9: + dependencies: + ms: 2.0.0 + optional: true + + debug@3.2.7: + dependencies: + ms: 2.1.3 + optional: true + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + dependency-graph@1.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + dom-accessibility-api@0.5.16: {} + + electron-to-chromium@1.5.433: {} + + emoji-regex@10.6.0: {} + + empathic@2.1.0: {} + + entities@8.1.0: {} + + errno@0.1.8: + dependencies: + prr: 1.0.1 + optional: true + + es-module-lexer@2.3.2: {} + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + esbuild@0.28.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.2 + '@esbuild/android-arm': 0.28.2 + '@esbuild/android-arm64': 0.28.2 + '@esbuild/android-x64': 0.28.2 + '@esbuild/darwin-arm64': 0.28.2 + '@esbuild/darwin-x64': 0.28.2 + '@esbuild/freebsd-arm64': 0.28.2 + '@esbuild/freebsd-x64': 0.28.2 + '@esbuild/linux-arm': 0.28.2 + '@esbuild/linux-arm64': 0.28.2 + '@esbuild/linux-ia32': 0.28.2 + '@esbuild/linux-loong64': 0.28.2 + '@esbuild/linux-mips64el': 0.28.2 + '@esbuild/linux-ppc64': 0.28.2 + '@esbuild/linux-riscv64': 0.28.2 + '@esbuild/linux-s390x': 0.28.2 + '@esbuild/linux-x64': 0.28.2 + '@esbuild/netbsd-arm64': 0.28.2 + '@esbuild/netbsd-x64': 0.28.2 + '@esbuild/openbsd-arm64': 0.28.2 + '@esbuild/openbsd-x64': 0.28.2 + '@esbuild/openharmony-arm64': 0.28.2 + '@esbuild/sunos-x64': 0.28.2 + '@esbuild/win32-arm64': 0.28.2 + '@esbuild/win32-ia32': 0.28.2 + '@esbuild/win32-x64': 0.28.2 + + escalade@3.2.0: {} + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + + expect-type@1.4.0: {} + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.8: {} + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + find-cache-directory@6.0.0: + dependencies: + common-path-prefix: 3.0.0 + pkg-dir: 8.0.0 + + find-up-simple@1.0.1: {} + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.2 + rollup: 4.63.4 + + flru@1.0.2: {} + + fsevents@2.3.3: + optional: true + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.7.0: {} + + graceful-fs@4.2.11: + optional: true + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.1 + transitivePeerDependencies: + - '@noble/hashes' + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + optional: true + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + optional: true + + immutable@5.1.9: {} + + import-meta-resolve@4.2.0: {} + + injection-js@2.6.1: + dependencies: + tslib: 2.8.1 + + is-extglob@2.1.1: + optional: true + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + optional: true + + is-interactive@2.0.0: {} + + is-potential-custom-element-name@1.0.1: {} + + is-unicode-supported@2.1.0: {} + + is-what@4.1.16: {} + + joycon@3.1.1: {} + + js-tokens@10.0.0: {} + + js-tokens@4.0.0: {} + + jsdom@27.4.0: + dependencies: + '@acemir/cssom': 0.9.31 + '@asamuzakjp/dom-selector': 6.8.1 + '@exodus/bytes': 1.15.1 + cssstyle: 5.3.7 + data-urls: 6.0.1 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 15.1.0 + ws: 8.21.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + - bufferutil + - supports-color + - utf-8-validate + + jsesc@3.1.0: {} + + json-schema-traverse@1.0.0: {} + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + less@4.9.1: + dependencies: + copy-anything: 3.0.5 + parse-node-version: 1.0.1 + optionalDependencies: + errno: 0.1.8 + graceful-fs: 4.2.11 + make-dir: 5.1.0 + mime: 1.6.0 + needle: 3.5.0 + probe-image-size: 7.4.0 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + + lightningcss-android-arm64@1.33.0: + optional: true + + lightningcss-darwin-arm64@1.33.0: + optional: true + + lightningcss-darwin-x64@1.33.0: + optional: true + + lightningcss-freebsd-x64@1.33.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + + lightningcss-linux-arm64-musl@1.33.0: + optional: true + + lightningcss-linux-x64-gnu@1.33.0: + optional: true + + lightningcss-linux-x64-musl@1.33.0: + optional: true + + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + + lightningcss-win32-x64-msvc@1.33.0: + optional: true + + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + lodash.merge@4.6.2: + optional: true + + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.2.0 + + lru-cache@11.5.3: {} + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + make-dir@5.1.0: + optional: true + + mdn-data@2.27.1: {} + + mime@1.6.0: + optional: true + + mimic-function@5.0.1: {} + + mlly@1.8.2: + dependencies: + acorn: 8.18.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.4 + + ms@2.0.0: + optional: true + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.19: {} + + needle@2.9.1: + dependencies: + debug: 3.2.7 + iconv-lite: 0.4.24 + sax: 1.6.1 + transitivePeerDependencies: + - supports-color + optional: true + + needle@3.5.0: + dependencies: + iconv-lite: 0.6.3 + sax: 1.6.1 + optional: true + + ng-packagr@22.1.1(@angular/compiler-cli@22.1.7(@angular/compiler@22.1.7)(typescript@6.0.3))(tslib@2.8.1)(typescript@6.0.3): + dependencies: + '@ampproject/remapping': 2.3.0 + '@angular/compiler-cli': 22.1.7(@angular/compiler@22.1.7)(typescript@6.0.3) + '@rollup/plugin-json': 6.1.0(rollup@4.63.4) + '@rollup/wasm-node': 4.63.4 + ajv: 8.20.0 + browserslist: 4.29.0 + chokidar: 5.0.0 + commander: 15.0.0 + dependency-graph: 1.0.0 + esbuild: 0.28.2 + find-cache-directory: 6.0.0 + injection-js: 2.6.1 + jsonc-parser: 3.3.1 + less: 4.9.1 + ora: 9.4.1 + piscina: 5.3.2 + postcss: 8.5.28 + rollup-plugin-dts: 6.4.1(rollup@4.63.4)(typescript@6.0.3) + rxjs: 7.8.2 + sass: 1.104.1 + tinyglobby: 0.2.17 + tslib: 2.8.1 + typescript: 6.0.3 + optionalDependencies: + rollup: 4.63.4 + transitivePeerDependencies: + - supports-color + + node-addon-api@7.1.1: + optional: true + + node-releases@2.0.56: {} + + object-assign@4.1.1: {} + + obug@2.2.1: {} + + onetime@7.0.0: + dependencies: + mimic-function: 5.0.1 + + ora@9.4.1: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.4.0 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.3.2 + string-width: 8.2.2 + + parse-node-version@1.0.1: {} + + parse5@8.0.1: + dependencies: + entities: 8.1.0 + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.7: {} + + pirates@4.0.7: {} + + piscina@5.3.2: + optionalDependencies: + '@napi-rs/nice': 1.1.1 + + pkg-dir@8.0.0: + dependencies: + find-up-simple: 1.0.1 + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.2 + pathe: 2.0.3 + + postcss-load-config@6.0.1(postcss@8.5.28): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.28 + + postcss@8.5.28: + dependencies: + nanoid: 3.3.19 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + probe-image-size@7.4.0: + dependencies: + lodash.merge: 4.6.2 + needle: 2.9.1 + stream-parser: 0.3.1 + transitivePeerDependencies: + - supports-color + optional: true + + prr@1.0.1: + optional: true + + punycode@2.3.1: {} + + react-dom@19.3.0(react@19.3.0): + dependencies: + react: 19.3.0 + scheduler: 0.28.0 + + react-is@17.0.2: {} + + react@19.3.0: {} + + readdirp@4.1.2: {} + + readdirp@5.1.1: {} + + reflect-metadata@0.2.2: {} + + require-from-string@2.0.2: {} + + resolve-from@5.0.0: {} + + restore-cursor@5.1.0: + dependencies: + onetime: 7.0.0 + signal-exit: 4.1.0 + + rolldown@1.2.9: + dependencies: + '@oxc-project/types': 0.150.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm-eabi': 1.2.9 + '@rolldown/binding-android-arm64': 1.2.9 + '@rolldown/binding-darwin-arm64': 1.2.9 + '@rolldown/binding-darwin-x64': 1.2.9 + '@rolldown/binding-freebsd-x64': 1.2.9 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.9 + '@rolldown/binding-linux-arm64-gnu': 1.2.9 + '@rolldown/binding-linux-arm64-musl': 1.2.9 + '@rolldown/binding-linux-ppc64-gnu': 1.2.9 + '@rolldown/binding-linux-s390x-gnu': 1.2.9 + '@rolldown/binding-linux-x64-gnu': 1.2.9 + '@rolldown/binding-linux-x64-musl': 1.2.9 + '@rolldown/binding-openharmony-arm64': 1.2.9 + '@rolldown/binding-win32-arm64-msvc': 1.2.9 + '@rolldown/binding-win32-x64-msvc': 1.2.9 + + rollup-plugin-dts@6.4.1(rollup@4.63.4)(typescript@6.0.3): + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.6.0 + convert-source-map: 2.0.0 + magic-string: 0.30.21 + rollup: 4.63.4 + typescript: 6.0.3 + optionalDependencies: + '@babel/code-frame': 7.29.7 + + rollup@4.63.4: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@rollup/rollup-android-arm-eabi': 4.63.4 + '@rollup/rollup-android-arm64': 4.63.4 + '@rollup/rollup-darwin-arm64': 4.63.4 + '@rollup/rollup-darwin-x64': 4.63.4 + '@rollup/rollup-freebsd-arm64': 4.63.4 + '@rollup/rollup-freebsd-x64': 4.63.4 + '@rollup/rollup-linux-arm-gnueabihf': 4.63.4 + '@rollup/rollup-linux-arm-musleabihf': 4.63.4 + '@rollup/rollup-linux-arm64-gnu': 4.63.4 + '@rollup/rollup-linux-arm64-musl': 4.63.4 + '@rollup/rollup-linux-loong64-gnu': 4.63.4 + '@rollup/rollup-linux-loong64-musl': 4.63.4 + '@rollup/rollup-linux-ppc64-gnu': 4.63.4 + '@rollup/rollup-linux-ppc64-musl': 4.63.4 + '@rollup/rollup-linux-riscv64-gnu': 4.63.4 + '@rollup/rollup-linux-riscv64-musl': 4.63.4 + '@rollup/rollup-linux-s390x-gnu': 4.63.4 + '@rollup/rollup-linux-x64-gnu': 4.63.4 + '@rollup/rollup-linux-x64-musl': 4.63.4 + '@rollup/rollup-openbsd-x64': 4.63.4 + '@rollup/rollup-openharmony-arm64': 4.63.4 + '@rollup/rollup-win32-arm64-msvc': 4.63.4 + '@rollup/rollup-win32-ia32-msvc': 4.63.4 + '@rollup/rollup-win32-x64-gnu': 4.63.4 + '@rollup/rollup-win32-x64-msvc': 4.63.4 + fsevents: 2.3.3 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safer-buffer@2.1.2: + optional: true + + sass@1.104.1: + dependencies: + chokidar: 5.0.0 + immutable: 5.1.9 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.6.0 + + sax@1.6.1: + optional: true + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.28.0: {} + + semver@7.8.5: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + source-map@0.6.1: + optional: true + + source-map@0.7.6: {} + + stackback@0.0.2: {} + + std-env@4.2.0: {} + + stdin-discarder@0.3.2: {} + + stream-parser@0.3.1: + dependencies: + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + optional: true + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.7.0 + strip-ansi: 7.2.0 + + string-width@8.2.2: + dependencies: + get-east-asian-width: 1.7.0 + strip-ansi: 7.2.0 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.3.0 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.17 + ts-interface-checker: 0.1.13 + + symbol-tree@3.2.4: {} + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyexec@1.3.1: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyrainbow@3.1.1: {} + + tldts-core@7.4.13: {} + + tldts@7.4.13: + dependencies: + tldts-core: 7.4.13 + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.13 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + tree-kill@1.2.2: {} + + ts-interface-checker@0.1.13: {} + + tslib@2.8.1: {} + + tsup@8.5.1(postcss@8.5.28)(typescript@6.0.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.7) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.7 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.28) + resolve-from: 5.0.0 + rollup: 4.63.4 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.17 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.28 + typescript: 6.0.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + typescript@6.0.3: {} + + ufo@1.6.4: {} + + update-browserslist-db@1.3.3(browserslist@4.29.0): + dependencies: + browserslist: 4.29.0 + escalade: 3.2.0 + picocolors: 1.1.1 + + use-sync-external-store@1.7.0(react@19.3.0): + dependencies: + react: 19.3.0 + + verkit@0.3.2: {} + + vite@8.3.0(esbuild@0.27.7)(less@4.9.1)(sass@1.104.1): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.7 + postcss: 8.5.28 + rolldown: 1.2.9 + tinyglobby: 0.2.17 + optionalDependencies: + esbuild: 0.27.7 + fsevents: 2.3.3 + less: 4.9.1 + sass: 1.104.1 + + vitest@4.1.11(jsdom@27.4.0)(vite@8.3.0(esbuild@0.27.7)(less@4.9.1)(sass@1.104.1)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@8.3.0(esbuild@0.27.7)(less@4.9.1)(sass@1.104.1)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.2.1 + pathe: 2.0.3 + picomatch: 4.0.7 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.1 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.1 + vite: 8.3.0(esbuild@0.27.7)(less@4.9.1)(sass@1.104.1) + why-is-node-running: 2.3.0 + optionalDependencies: + jsdom: 27.4.0 + transitivePeerDependencies: + - msw + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@4.0.0: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@15.1.0: + dependencies: + tr46: 6.0.0 + webidl-conversions: 8.0.1 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + ws@8.21.3: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + y18n@5.0.8: {} + + yargs-parser@22.0.0: {} + + yargs@18.1.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 8.2.2 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yoctocolors@2.2.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..6c1ec89 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,6 @@ +packages: + - packages/* + +allowBuilds: + esbuild: true + '@parcel/watcher': false diff --git a/test/index.js b/test/index.js deleted file mode 100644 index 680702f..0000000 --- a/test/index.js +++ /dev/null @@ -1,10 +0,0 @@ -'use strict'; - -var assert = require('assert'); -//var gtvBfy = require('../lib'); - -describe('gtv-bfy', function () { - it('should have unit test!', function () { - assert(false, 'we expected this package author to add actual unit tests.'); - }); -}); diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..a6026fe --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + // tsup's d.ts build injects `baseUrl`, deprecated in TypeScript 6. + "ignoreDeprecations": "6.0", + "target": "ES2017", + "lib": ["ES2017", "DOM"], + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "jsx": "react-jsx" + } +} diff --git a/tsup.base.ts b/tsup.base.ts new file mode 100644 index 0000000..750d599 --- /dev/null +++ b/tsup.base.ts @@ -0,0 +1,11 @@ +import type { Options } from 'tsup' + +/** Shared build: ESM + CJS + types, downleveled for Tizen 5 (Chromium 63) and webOS 5 (Chromium 68). */ +export const baseConfig: Options = { + entry: ['src/index.ts'], + format: ['esm', 'cjs'], + dts: true, + clean: true, + sourcemap: true, + target: 'chrome63' +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..c67036a --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,23 @@ +import { fileURLToPath } from 'node:url' +import { defineConfig } from 'vitest/config' + +// Tests run against the sources of sibling packages, no build needed. +const src = (pkg: string) => fileURLToPath(new URL(`./packages/${pkg}/src/index.ts`, import.meta.url)) +const alias = { + '@arxis/gtvzone-core': src('core'), + '@arxis/gtvzone-dom': src('dom') +} + +export default defineConfig({ + test: { + projects: [ + { resolve: { alias }, test: { name: 'core', root: 'packages/core', environment: 'node' } }, + { resolve: { alias }, test: { name: 'dom', root: 'packages/dom', environment: 'jsdom' } }, + { resolve: { alias }, test: { name: 'react', root: 'packages/react', environment: 'jsdom' } }, + { + resolve: { alias }, + test: { name: 'angular', root: 'packages/angular', environment: 'jsdom', setupFiles: ['src/test-setup.ts'] } + } + ] + } +})