diff --git a/.npmrc.example b/.npmrc.example index d3df147..3416dc1 100644 --- a/.npmrc.example +++ b/.npmrc.example @@ -7,13 +7,13 @@ # Redirige solo el scope @pi al repositorio npm (hosted) de Nexus. # Cambia la URL por la de tu instancia y el nombre de tu repo npm-hosted. -@pi:registry=https://nexus.tuempresa.com/repository/npm-private/ +@pi:registry=https://nexus.pireon.dev/repository/npm-private/ # Autenticación por token (recomendado, funciona igual en local y en CI). # El token se genera en Nexus: usuario > Security > NuGet/npm token, o vía -# `npm login --registry=https://nexus.tuempresa.com/repository/npm-private/` +# `npm login --registry=https://nexus.pireon.dev/repository/npm-private/` # (esto último escribe la línea de abajo automáticamente en tu ~/.npmrc). -//nexus.tuempresa.com/repository/npm-private/:_authToken=${NEXUS_NPM_TOKEN} +//nexus.pireon.dev/repository/npm-private/:_authToken=${NEXUS_NPM_TOKEN} # Nexus no es el registro público de npm: sin este flag, `npm publish` # rechaza paquetes con scope si no lleva --access, y con un registro privado diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000..f87cdca --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,13 @@ +{ + "plugins": ["./tools/stylelint-plugin-pi-theme/no-raw-color-token.mjs"], + "customSyntax": "postcss-scss", + "ignoreFiles": [ + "libs/ui/src/lib/styles/themes/**/*.scss", + "libs/ui/src/lib/styles/common/abstracts/**/*.scss", + "dist/**", + "node_modules/**" + ], + "rules": { + "pi/no-raw-color-token": true + } +} diff --git a/apps/playground/src/app/app.html b/apps/playground/src/app/app.html index 3497039..288ee4c 100644 --- a/apps/playground/src/app/app.html +++ b/apps/playground/src/app/app.html @@ -16,9 +16,9 @@ @@ -26,7 +26,7 @@ @@ -43,8 +43,8 @@

- Tema: {{ theme() }} · - {{ shellClassByTheme[theme()] }} + Tema: {{ themeService.theme() }} · + {{ themeService.shellClass() }} en el <body>. Los cuatro temas comparten exactamente los mismos selectores: solo cambian los tokens --pi-color-* y --pi-theme-* que definen themes/_default.scss, diff --git a/apps/playground/src/app/app.ts b/apps/playground/src/app/app.ts index 4b8d232..b2b9821 100644 --- a/apps/playground/src/app/app.ts +++ b/apps/playground/src/app/app.ts @@ -1,14 +1,12 @@ -import { DOCUMENT } from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - effect, - inject, - signal, -} from '@angular/core'; +import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; -import { PiLoaderBarComponent, PiSlideToggleComponent, ShellClass } from '@pi/ui'; +import { + PiLoaderBarComponent, + PiSlideToggleComponent, + PiThemeService, + ThemeVariant, +} from '@pi/ui'; /** * Shell del playground: cabecera, navegación y estado de tema. El contenido de @@ -38,9 +36,12 @@ export class App { /* --- Tema y modo oscuro -------------------------------------------------- */ - private readonly document = inject(DOCUMENT); - - protected readonly darkMode = signal(false); + /** + * Aplicar `pi-shell pi-shell__` en `` y `dark-mode` en `` + * (con persistencia en `localStorage`) vive en `PiThemeService`, no aquí: + * cualquier app que consuma `@pi/ui` necesita este mismo cableado. + */ + protected readonly themeService = inject(PiThemeService); protected readonly themeOptions: ReadonlyArray<{ value: ThemeVariant; @@ -51,34 +52,4 @@ export class App { { value: 'minimal', label: 'Minimal' }, { value: 'gradient', label: 'Gradient' }, ]; - - /** - * Tema visual de la librería. `ShellClass` devuelve las dos clases que hay - * que poner juntas (`pi-shell pi-shell__`); van en el `` porque - * los overlays de Material (diálogos, snackbars) se montan fuera de - * `` y también deben heredarlas. - */ - protected readonly theme = signal('default'); - - protected readonly shellClassByTheme: Record = { - default: ShellClass.Default, - alternative: ShellClass.Alternative, - minimal: ShellClass.Minimal, - gradient: ShellClass.Gradient, - }; - - constructor() { - effect(() => { - this.document.documentElement.classList.toggle( - 'dark-mode', - this.darkMode(), - ); - }); - - effect(() => { - this.document.body.className = this.shellClassByTheme[this.theme()]; - }); - } } - -type ThemeVariant = 'default' | 'alternative' | 'minimal' | 'gradient'; diff --git a/libs/ui/README.md b/libs/ui/README.md index acdd8b3..e384b20 100644 --- a/libs/ui/README.md +++ b/libs/ui/README.md @@ -80,6 +80,24 @@ en custom properties (`--pi-color-*`, `--pi-theme-*`) aplicadas vía `ShellClass tema (Default/Alternative/Minimal/Gradient) y guía para añadir tokens o temas nuevos: [`TOKENS.md`](./TOKENS.md). +Tres piezas pensadas para que reskinear la librería en otro proyecto sea +barato: + +- **`PiThemeService`** (`util/theme`) — inyectable, con `theme`/`darkMode` + como señales normales (mutables con setters o con `[(value)]`/`[(checked)]` + directo), persistencia en `localStorage` y aplicación de clases vía + `effect()`. Sustituye al cableado manual que antes vivía en cada app + consumidora (ver el `app.ts` del playground antes/después de esta + extracción). +- **`nx g @pi/ui:theme `** — genera `_.scss`, lo registra en + `themes/index.scss`, añade la entrada a `ShellClass` y extiende + `PiThemeService`; el tema queda seleccionable sin pasos manuales. +- **`nx run ui:lint-styles`** — regla de stylelint (`pi/no-raw-color-token`) + que detecta un `--pi-color-*` usado fuera del fallback de un + `--pi-theme-*`, la misma clase de bug que dejó colores de marca fijos en + modo oscuro (accordion/tab/select-option, antes de tokenizarlos). Target + aparte por ahora — ver TOKENS.md para el porqué y el estado de adopción. + | Componente | Contrato | Tipo del valor | | ---------------------------------------------------- | --------------------- | ------------------------------------- | | text, email, password, phone, icon, search, textarea | `FormValueControl` | `string` | diff --git a/libs/ui/TOKENS.md b/libs/ui/TOKENS.md index 1d5e214..7026b23 100644 --- a/libs/ui/TOKENS.md +++ b/libs/ui/TOKENS.md @@ -20,10 +20,31 @@ componente — solo redefine tokens (ver `_alternative.scss`, `_minimal.scss`, ## Cómo se aplica un tema +La forma recomendada es `PiThemeService` — inyectable, con persistencia en +`localStorage` y sin tener que tocar el DOM a mano: + +```ts +import { inject } from '@angular/core'; +import { PiThemeService } from '@pi/ui'; + +const themeService = inject(PiThemeService); + +themeService.setTheme('alternative'); // pinta pi-shell pi-shell__alternative en +themeService.toggleDarkMode(); // pinta/quita dark-mode en +``` + +`theme`/`darkMode` son señales normales: también se pueden mutar con un +`[(value)]`/`[(checked)]` dos vías directo sobre la señal (ver el uso real en +`apps/playground/src/app/app.ts`/`app.html`). Por debajo, cada cambio dispara +un `effect()` que aplica la clase y persiste — cualquier camino de escritura +queda cubierto por igual, no solo los setters. + +Si no quieres el servicio (p. ej. fuera de un contexto de inyección), el +mecanismo de bajo nivel sigue disponible: + ```ts import { ShellClass } from '@pi/ui'; -// en el elemento raíz (normalmente o el contenedor de la app) document.body.className = ShellClass.Alternative; // 'pi-shell pi-shell__alternative' ``` @@ -122,6 +143,8 @@ siempre el *fallback* de `var()` (el valor que tenían antes de tokenizar). | `--pi-theme-tooltip-radius` | `var(--pi-border-radius-medium)` | `var(--pi-border-radius-xx-small)` | `_mat-tooltip.scss` | | `--pi-theme-tooltip-shadow` | `var(--pi-box-shadow-panel-double)` | `none` | `_mat-tooltip.scss` | | `--pi-theme-accordion-radius` | `var(--pi-border-radius-large)` | `var(--pi-border-radius-xx-small)` | `accordion.component.scss`, `_horizontal-accordion.scss`, `_vertical-accordion.scss` (×6, incluye un `calc(... - 0.0625rem)`) | +| `--pi-theme-overlay-divider-color` | `var(--pi-color-grey-100)` | — | `drawer.component.scss`, `hub.component.scss` (borde bajo la cabecera) | +| `--pi-theme-hub-grabber-color` | `var(--pi-color-grey-100)` | — | `hub.component.scss` (el asa superior) | ### Puente con Angular Material (`--mat-sys-*`) @@ -236,12 +259,55 @@ compilación y no forman parte del contrato de theming en runtime. ## Cómo añadir un tema nuevo +La forma recomendada es el generador: + +```bash +nx g @pi/ui:theme mi-marca +``` + +Crea `libs/ui/src/lib/styles/themes/_mi-marca.scss` (en blanco, con los +grupos de tokens más habituales comentados a modo de chuleta), lo registra en +`themes/index.scss`, añade la entrada a `ShellClass` en `styles/configs.ts` y +extiende `ThemeVariant`/`PiThemeService` — el tema queda seleccionable con +`themeService.setTheme('mi-marca')` sin ningún paso manual más. Solo falta +descomentar y ajustar los tokens que definen el aspecto, y (si tu app expone un +selector de tema) añadirlo a esa lista. + +Estos son los pasos que da por ti, si prefieres hacerlo a mano o entender qué +toca: + 1. Crea `libs/ui/src/lib/styles/themes/_.scss` con un mixin `ThemeTokens` que redefina solo los tokens `--pi-theme-*` (y, si aplica, `--pi-color-*`) que cambian respecto a Default. 2. Aplícalo a `.pi-shell__`. 3. Impórtalo desde `themes/index.scss`. 4. Añade la entrada a `ShellClass` en `styles/configs.ts`. +5. Añade `''` a `ThemeVariant` y una entrada a `shellClassByVariant` + en `util/theme/theme.service.ts`. No hace falta tocar ningún componente: si consumen los tokens en vez de valores literales, el tema nuevo se propaga solo. + +## Lint de tokens: evitar que un color de marca se filtre sin querer + +El bug que motivó tokenizar accordion/tab/select-option/drawer/hub esta misma +sesión (un `var(--pi-color-*)` a pelo en el componente, así que ningún tema ni +el modo oscuro puede redefinir esa superficie) tiene ahora una regla de +stylelint que lo detecta automáticamente: `pi/no-raw-color-token` +(`tools/stylelint-plugin-pi-theme/`). + +```bash +nx run ui:lint-styles +``` + +Marca cualquier `--pi-color-*` que aparezca fuera del *fallback* de un +`var(--pi-theme-*, ...)` — es decir, exactamente el patrón contrario al de +["El patrón de fallback"](#el-patrón-de-fallback). Están exentos +`styles/themes/**` (ahí `--pi-color-*` es legítimo: son las definiciones) y +`styles/common/abstracts/**` (donde se declaran las primitivas de la paleta). + +**Estado actual: es un target aparte, no está enganchado a `nx lint` ni a CI.** +Al escribirse, señala ~500 usos preexistentes repartidos por componentes que +esta sesión no llegó a tokenizar — adoptarlo como gate exige ir migrándolos +primero (mismo patrón que en "Cómo tokenizar una superficie nueva"). Úsalo para +no introducir *más* mientras se migra el resto. diff --git a/libs/ui/TOKENS.pdf b/libs/ui/TOKENS.pdf index 020e553..7426581 100644 Binary files a/libs/ui/TOKENS.pdf and b/libs/ui/TOKENS.pdf differ diff --git a/libs/ui/docs/theming.html b/libs/ui/docs/theming.html index 4aa6558..6733f1d 100644 --- a/libs/ui/docs/theming.html +++ b/libs/ui/docs/theming.html @@ -454,19 +454,32 @@ --pi-theme-tooltip-radiussin declarar → var(--pi-border-radius-medium)_mat-tooltip.scss --pi-theme-tooltip-shadowsin declarar → var(--pi-box-shadow-panel-double)_mat-tooltip.scss --pi-theme-accordion-radiussin declarar → var(--pi-border-radius-large)accordion.component.scss, _horizontal-accordion.scss, _vertical-accordion.scss (×6, incluye un calc()) + --pi-theme-overlay-divider-colorsin declarar → var(--pi-color-grey-100)drawer.component.scss, hub.component.scss + --pi-theme-hub-grabber-colorsin declarar → var(--pi-color-grey-100)hub.component.scss

- Minimal declara los seis en su mixin (_minimal.scss): aplana - todos los radios a --pi-border-radius-xx-small y apaga las dos - sombras (none) — mismo criterio que ya aplicaba a botón, campo - y card, extendido al resto de superficies flotantes. + Minimal declara los seis primeros en su mixin (_minimal.scss): + aplana todos los radios a --pi-border-radius-xx-small y apaga + las dos sombras (none) — mismo criterio que ya aplicaba a + botón, campo y card, extendido al resto de superficies flotantes. Los dos + últimos (drawer/hub) son más recientes y todavía ningún tema los redefine.

3Crear un tema, paso a paso

Cuatro ficheros, ninguno toca un componente. Ejemplo con un tema ficticio brutalist.

+
+ Los cuatro pasos de esta página los da un generador: + nx g @pi/ui:theme brutalist. Crea el fichero del paso 1 (en + blanco, con los grupos de tokens más habituales comentados), hace los + pasos 2 y 3, y además extiende PiThemeService (paso 4) para + que el tema quede seleccionable sin ningún paso manual más. Lo que sigue + es para quien prefiera hacerlo a mano o quiera entender qué toca cada + paso. +
+
1
@@ -523,7 +536,13 @@
4
Aplícalo en la app consumidora
-

En el playground esto ya está resuelto con un signal + effect (ver apps/playground/src/app/app.ts); en cualquier otra app basta con:

+

Con PiThemeService (ver apps/playground/src/app/app.ts) basta con:

+
import { inject } from '@angular/core';
+import { PiThemeService } from '@pi/ui';
+
+const themeService = inject(PiThemeService);
+themeService.setTheme('brutalist');
+

Si el generador ya extendió ThemeVariant/shellClassByVariant (paso automático 4), no hace falta tocar nada más que este setTheme. Sin el servicio, el mecanismo de bajo nivel sigue funcionando igual:

import { ShellClass } from '@pi/ui';
 
 document.body.className = ShellClass.Brutalist;
@@ -654,7 +673,11 @@ document.body.className = ShellClass.Brutalist; @pi/ui no trae un tema oscuro propio. El playground activa uno "best-effort" añadiendo la clase dark-mode al <html> (no al <body>, que es donde - vive .pi-shell), y ahí reasigna solo los + vive .pi-shell) — hoy vía + PiThemeService.setDarkMode()/toggleDarkMode(), + que hace ese toggle por ti (persistido en localStorage) en + vez de que cada app repita el mismo effect() — y ahí reasigna + solo los neutros (--pi-color-white, --pi-color-grey-*, --pi-color-black) — las paletas de marca (primary, accent…) se dejan igual: @@ -684,12 +707,14 @@ document.body.className = ShellClass.Brutalist;

Antes de dar el tema por terminado

    +
  • Se creó con nx g @pi/ui:theme <nombre> — hace los tres puntos siguientes por ti.
  • El fichero nuevo vive en themes/, exporta un mixin y una clase .pi-shell__<nombre>.
  • Está en themes/index.scss, al final de ambos bloques (@forward y @use).
  • -
  • Tiene entrada en ShellClass (configs.ts), con un comentario de una línea que resuma el aspecto.
  • +
  • Tiene entrada en ShellClass (configs.ts) y en PiThemeService (ThemeVariant/shellClassByVariant), con un comentario de una línea que resuma el aspecto.
  • Solo declara los tokens que cambia — el resto hereda de _default.scss sin repetirlo.
  • Si toca --pi-color-primary/accent/highlighted, revisado que el puente --mat-sys-* (declarado una vez en default) sigue coherente.
  • Si toca overlays (modal/notificación/tooltip/accordion), revisado con las tres páginas de catálogo, no solo con un botón suelto.
  • +
  • Si al tokenizar una superficie nueva tocaste un .component.scss, nx run ui:lint-styles no marca ese fichero (detecta --pi-color-* usado fuera de un fallback --pi-theme-*).
  • Probado en el playground con las tres páginas de catálogo, no solo en un botón suelto.
diff --git a/libs/ui/docs/theming.pdf b/libs/ui/docs/theming.pdf index 9b29be9..6101df5 100644 Binary files a/libs/ui/docs/theming.pdf and b/libs/ui/docs/theming.pdf differ diff --git a/libs/ui/generators.json b/libs/ui/generators.json new file mode 100644 index 0000000..7792934 --- /dev/null +++ b/libs/ui/generators.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://raw.githubusercontent.com/nrwl/nx/master/packages/nx/schemas/generators.json", + "generators": { + "theme": { + "factory": "./src/generators/theme/generator.cts", + "schema": "./src/generators/theme/schema.json", + "description": "Crea un nuevo tema (_.scss + ShellClass + PiThemeService) para @pi/ui." + } + } +} diff --git a/libs/ui/ng-package.json b/libs/ui/ng-package.json index db0c4c0..340c3ba 100644 --- a/libs/ui/ng-package.json +++ b/libs/ui/ng-package.json @@ -3,5 +3,13 @@ "dest": "../../dist/libs/ui", "lib": { "entryFile": "src/index.ts" - } + }, + "allowedNonPeerDependencies": ["lodash", "luxon", "ngx-infinite-scroll"], + "assets": [ + { + "glob": "**/*.scss", + "input": "src/lib/styles", + "output": "/styles" + } + ] } diff --git a/libs/ui/package.json b/libs/ui/package.json index 7dad134..eb924c5 100644 --- a/libs/ui/package.json +++ b/libs/ui/package.json @@ -1,9 +1,29 @@ { "name": "@pi/ui", "version": "0.0.1", + "description": "Librería de componentes Angular standalone (botones, campos con Signal Forms, cards, charts, drawer/hub/modal, theming vía tokens CSS...).", + "license": "MIT", + "generators": "./generators.json", + "publishConfig": { + "registry": "https://nexus.pireon.dev/repository/npm-private/", + "access": "restricted" + }, "peerDependencies": { + "@angular/animations": "^22.0.0", + "@angular/cdk": "^22.0.0", "@angular/common": "^22.0.0", - "@angular/core": "^22.0.0" + "@angular/core": "^22.0.0", + "@angular/forms": "^22.0.0", + "@angular/material": "^22.0.0", + "@angular/router": "^22.0.0", + "@ngx-translate/core": "^18.0.0", + "@ngx-translate/http-loader": "^18.0.0", + "chart.js": "^4.0.0" + }, + "dependencies": { + "lodash": "^4.17.0", + "luxon": "^3.0.0", + "ngx-infinite-scroll": "^22.0.0" }, "sideEffects": false } diff --git a/libs/ui/project.json b/libs/ui/project.json index 8da1cee..d181740 100644 --- a/libs/ui/project.json +++ b/libs/ui/project.json @@ -52,6 +52,27 @@ }, "lint": { "executor": "@nx/eslint:lint" + }, + "lint-styles": { + "executor": "nx:run-commands", + "options": { + "command": "stylelint \"libs/ui/src/**/*.scss\" --config .stylelintrc.json" + } + }, + "build-generators": { + "executor": "nx:run-commands", + "dependsOn": ["package"], + "outputs": [ + "{workspaceRoot}/dist/{projectRoot}/generators", + "{workspaceRoot}/dist/{projectRoot}/generators.json" + ], + "options": { + "commands": [ + "tsc -p libs/ui/tsconfig.generators.json", + "node libs/ui/scripts/finalize-generators-dist.cjs" + ], + "parallel": false + } } } } diff --git a/libs/ui/scripts/finalize-generators-dist.cjs b/libs/ui/scripts/finalize-generators-dist.cjs new file mode 100644 index 0000000..ef3374d --- /dev/null +++ b/libs/ui/scripts/finalize-generators-dist.cjs @@ -0,0 +1,55 @@ +'use strict'; + +/** + * `ng-packagr` (el `build`/`package` de `ui`) solo empaqueta lo alcanzable + * desde `src/index.ts` — los generadores (`src/generators/`) son Node/Nx + * plugin, no Angular, así que ng-packagr los ignora por completo. Este script + * los deja listos en `dist/libs/ui/` (ya compilados por + * `tsc -p tsconfig.generators.json`, que corre antes que este script) para + * que `generators.json` publicado apunte a ficheros que de verdad existen en + * el tarball — si no, `nx g @pi/ui:theme` funcionaría dentro de este + * monorepo pero fallaría para quien instale el paquete desde el registry. + */ + +const fs = require('fs'); +const path = require('path'); + +const libRoot = path.join(__dirname, '..'); +const distRoot = path.join(libRoot, '..', '..', 'dist', 'libs', 'ui'); + +const sourceManifest = JSON.parse( + fs.readFileSync(path.join(libRoot, 'generators.json'), 'utf8'), +); + +/** `./src/generators/theme/generator.cts` (fuente) -> `./generators/theme/generator.cjs` (dist). */ +function toDistPath(sourcePath) { + return sourcePath.replace('./src/generators/', './generators/').replace(/\.cts$/, '.cjs'); +} + +const distManifest = { + ...sourceManifest, + generators: Object.fromEntries( + Object.entries(sourceManifest.generators).map(([name, config]) => [ + name, + { + ...config, + factory: toDistPath(config.factory), + schema: toDistPath(config.schema), + }, + ]), + ), +}; + +fs.writeFileSync( + path.join(distRoot, 'generators.json'), + JSON.stringify(distManifest, null, 2) + '\n', +); + +for (const { schema } of Object.values(sourceManifest.generators)) { + const sourceSchemaPath = path.join(libRoot, schema); + const destSchemaPath = path.join(distRoot, toDistPath(schema)); + fs.mkdirSync(path.dirname(destSchemaPath), { recursive: true }); + fs.copyFileSync(sourceSchemaPath, destSchemaPath); +} + +console.log('✔ generators.json + schemas listos en dist/libs/ui/'); diff --git a/libs/ui/src/generators/theme/generator.cts b/libs/ui/src/generators/theme/generator.cts new file mode 100644 index 0000000..c544fd8 --- /dev/null +++ b/libs/ui/src/generators/theme/generator.cts @@ -0,0 +1,172 @@ +import { Tree, formatFiles, logger, names } from '@nx/devkit'; +import type { ThemeGeneratorSchema } from './schema'; + +const THEMES_DIR = 'libs/ui/src/lib/styles/themes'; +const CONFIGS_PATH = 'libs/ui/src/lib/styles/configs.ts'; +const THEME_SERVICE_PATH = 'libs/ui/src/lib/util/theme/theme.service.ts'; + +export default async function themeGenerator( + tree: Tree, + options: ThemeGeneratorSchema, +): Promise { + const { fileName: themeName, className, propertyName } = names(options.name); + const scssPath = `${THEMES_DIR}/_${themeName}.scss`; + + if (tree.exists(scssPath)) { + throw new Error( + `Ya existe un tema en ${scssPath}. Elige otro nombre o bórralo primero.`, + ); + } + + tree.write(scssPath, buildThemeScss(themeName, propertyName)); + registerInThemesIndex(tree, themeName); + addShellClassEntry(tree, className, themeName); + extendThemeService(tree, className, themeName); + + await formatFiles(tree); + + logger.info(`\n✔ Tema "${themeName}" creado:`); + logger.info(` - ${scssPath}`); + logger.info(` - registrado en ${THEMES_DIR}/index.scss`); + logger.info(` - añadido a ShellClass en ${CONFIGS_PATH}`); + logger.info(` - añadido a ThemeVariant/PiThemeService en ${THEME_SERVICE_PATH}`); + logger.info( + `\nEdita los tokens de ${scssPath} para definir el aspecto (ver TOKENS.md).`, + ); + logger.info( + `Ya es seleccionable vía PiThemeService.setTheme('${themeName}'); solo falta` + + ` añadirlo a la UI de tu app (p. ej. \`themeOptions\` en tu shell) si quieres un botón.`, + ); +} + +function buildThemeScss(themeName: string, propertyName: string): string { + return `// TEMA ${themeName.toUpperCase()} +// +// Generado con "nx g @pi/ui:theme ${themeName}". Empieza en blanco a +// propósito: mientras no descomentes nada, este tema es visualmente idéntico +// al tema por defecto (todo cae al ".pi-shell" base). Descomenta y cambia solo +// los tokens que quieras redefinir — no hace falta declarar el resto. Lista +// completa de tokens "--pi-theme-*"/"--pi-color-*" disponibles: TOKENS.md. + +@mixin ${propertyName}ThemeTokens { + // --- Paleta de marca ---------------------------------------------------- + // --pi-color-primary: #799ef0; + // --pi-color-accent: #eda82a; + // --pi-color-highlighted: #0d2a6b; + + // --- Botones -------------------------------------------------------------- + // --pi-theme-button-radius: 999px; + // --pi-theme-button-shadow: none; + // --pi-theme-button-fill-primary: none; + + // --- Campos de formulario --------------------------------------------------- + // --pi-theme-field-radius: 0.875rem; + // --pi-theme-field-border-color-focus: var(--pi-color-primary); + // --pi-theme-field-surface: var(--pi-color-white); + + // --- Superficies ------------------------------------------------------------ + // --pi-theme-card-radius: var(--pi-border-radius-large); + // --pi-theme-card-shadow: var(--pi-box-shadow-panel); + // --pi-theme-panel-radius: var(--pi-border-radius-large); +} + +.pi-shell__${themeName} { + @include ${propertyName}ThemeTokens; +} +`; +} + +function registerInThemesIndex(tree: Tree, themeName: string): void { + const path = `${THEMES_DIR}/index.scss`; + const content = tree.read(path, 'utf-8'); + if (!content) throw new Error(`No se encontró ${path}.`); + + const withForward = insertAfterLastMatch( + content, + /@forward '[^']+';/g, + `@forward '${themeName}';`, + ); + const withUse = insertAfterLastMatch( + withForward, + /@use '[^']+' as \*;/g, + `@use '${themeName}' as *;`, + ); + + tree.write(path, withUse); +} + +function addShellClassEntry( + tree: Tree, + className: string, + themeName: string, +): void { + patchObjectLiteral( + tree, + CONFIGS_PATH, + /export const ShellClass = \{[\s\S]*?\n\};/, + ` /** Generado con \`nx g @pi/ui:theme ${themeName}\`. */\n ${className}: shellTheming('${themeName}'),\n};`, + ); +} + +function extendThemeService( + tree: Tree, + className: string, + themeName: string, +): void { + const path = THEME_SERVICE_PATH; + const content = tree.read(path, 'utf-8'); + if (!content) throw new Error(`No se encontró ${path}.`); + + const unionPattern = + /export type ThemeVariant = 'default' \| 'alternative' \| 'minimal' \| 'gradient'(?:\s*\|\s*'[^']+')*;/; + const updatedUnion = content.replace(unionPattern, (match) => + match.replace(/;$/, ` | '${themeName}';`), + ); + + patchObjectLiteral( + tree, + path, + /const shellClassByVariant: Record = \{[\s\S]*?\n\};/, + ` '${themeName}': ShellClass.${className},\n};`, + updatedUnion, + ); +} + +/** + * Sustituye el objeto literal que casa con `blockPattern` insertando + * `newClosing` justo antes de su `\n};` de cierre — funciona igual si el + * generador ya se ha ejecutado antes (siempre inserta antes del PRIMER cierre + * tras la apertura, no del último del fichero). + */ +function patchObjectLiteral( + tree: Tree, + path: string, + blockPattern: RegExp, + newClosing: string, + contentOverride?: string, +): void { + const content = contentOverride ?? tree.read(path, 'utf-8'); + if (!content) throw new Error(`No se encontró ${path}.`); + + const match = content.match(blockPattern); + if (!match) { + throw new Error(`No se encontró el bloque esperado en ${path}.`); + } + + const block = match[0]; + const updatedBlock = block.replace(/\n\};$/, `\n${newClosing}`); + tree.write(path, content.replace(block, updatedBlock)); +} + +function insertAfterLastMatch( + content: string, + pattern: RegExp, + newLine: string, +): string { + const matches = [...content.matchAll(pattern)]; + if (matches.length === 0) return `${content}\n${newLine}`; + + const last = matches[matches.length - 1]; + const insertPos = (last.index ?? content.length) + last[0].length; + return `${content.slice(0, insertPos)}\n${newLine}${content.slice(insertPos)}`; +} diff --git a/libs/ui/src/generators/theme/schema.d.ts b/libs/ui/src/generators/theme/schema.d.ts new file mode 100644 index 0000000..6653e3f --- /dev/null +++ b/libs/ui/src/generators/theme/schema.d.ts @@ -0,0 +1,3 @@ +export interface ThemeGeneratorSchema { + name: string; +} diff --git a/libs/ui/src/generators/theme/schema.json b/libs/ui/src/generators/theme/schema.json new file mode 100644 index 0000000..bf5ab36 --- /dev/null +++ b/libs/ui/src/generators/theme/schema.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/schema", + "$id": "PiUiTheme", + "title": "Genera un nuevo tema para @pi/ui", + "description": "Crea _.scss, lo registra en themes/index.scss, añade una entrada a ShellClass (styles/configs.ts) y extiende ThemeVariant/PiThemeService para que quede utilizable de inmediato.", + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Nombre del tema, en kebab-case (p. ej. \"acme-corp\").", + "$default": { "$source": "argv", "index": 0 }, + "x-prompt": "¿Cómo se llama el tema?" + } + }, + "required": ["name"] +} diff --git a/libs/ui/src/index.ts b/libs/ui/src/index.ts index e544242..1250bc3 100644 --- a/libs/ui/src/index.ts +++ b/libs/ui/src/index.ts @@ -6,7 +6,6 @@ export * from './lib/util/affix'; export * from './lib/util/amplitude'; export * from './lib/util/color'; export * from './lib/util/common'; -export * from './lib/util/dato-cms'; export * from './lib/util/device-configs'; export * from './lib/util/environments'; export * from './lib/util/icon-registry'; @@ -21,6 +20,7 @@ export * from './lib/util/router-store'; export * from './lib/util/schema'; export * from './lib/util/scroll-to'; export * from './lib/util/status'; +export * from './lib/util/theme'; export * from './lib/util/translate'; export * from './lib/util/validator'; export * from './lib/util/value'; diff --git a/libs/ui/src/lib/ui/drawer/drawer/drawer.component.scss b/libs/ui/src/lib/ui/drawer/drawer/drawer.component.scss index 84d4b2a..7452578 100644 --- a/libs/ui/src/lib/ui/drawer/drawer/drawer.component.scss +++ b/libs/ui/src/lib/ui/drawer/drawer/drawer.component.scss @@ -10,7 +10,7 @@ justify-content: space-between; gap: var(--pi-spacing-medium); padding: var(--pi-spacing-large) var(--pi-spacing-x-large); - border-bottom: 0.0625rem solid var(--pi-color-grey-100); + border-bottom: 0.0625rem solid var(--pi-theme-overlay-divider-color, var(--pi-color-grey-100)); } main { diff --git a/libs/ui/src/lib/ui/hub/hub/hub.component.scss b/libs/ui/src/lib/ui/hub/hub/hub.component.scss index 6bd67c1..e616447 100644 --- a/libs/ui/src/lib/ui/hub/hub/hub.component.scss +++ b/libs/ui/src/lib/ui/hub/hub/hub.component.scss @@ -10,7 +10,7 @@ height: 0.25rem; margin: var(--pi-spacing-small) auto 0; border-radius: var(--pi-border-radius-x-small); - background-color: var(--pi-color-grey-100); + background-color: var(--pi-theme-hub-grabber-color, var(--pi-color-grey-100)); } header { @@ -19,7 +19,7 @@ justify-content: space-between; gap: var(--pi-spacing-medium); padding: var(--pi-spacing-medium) var(--pi-spacing-x-large) var(--pi-spacing-large); - border-bottom: 0.0625rem solid var(--pi-color-grey-100); + border-bottom: 0.0625rem solid var(--pi-theme-overlay-divider-color, var(--pi-color-grey-100)); } main { diff --git a/libs/ui/src/lib/util/dato-cms/graphql.module.ts b/libs/ui/src/lib/util/dato-cms/graphql.module.ts deleted file mode 100644 index 2686458..0000000 --- a/libs/ui/src/lib/util/dato-cms/graphql.module.ts +++ /dev/null @@ -1 +0,0 @@ -export const datoCms = 'https://graphql.datocms.com/'; // our GraphQL API diff --git a/libs/ui/src/lib/util/dato-cms/index.ts b/libs/ui/src/lib/util/dato-cms/index.ts deleted file mode 100644 index ef59679..0000000 --- a/libs/ui/src/lib/util/dato-cms/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './util-dato-cms.module'; diff --git a/libs/ui/src/lib/util/dato-cms/util-dato-cms.module.ts b/libs/ui/src/lib/util/dato-cms/util-dato-cms.module.ts deleted file mode 100644 index ac99c64..0000000 --- a/libs/ui/src/lib/util/dato-cms/util-dato-cms.module.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core'; - -// Apollo -import { InMemoryCache } from '@apollo/client/core'; -import { APOLLO_OPTIONS } from 'apollo-angular'; -import { HttpLink } from 'apollo-angular/http'; - -// Shared -import { environment } from '../environments'; - -// datoCMS -import { datoCms } from './graphql.module'; - -/** - * Sustituye a `PiSharedUtilDatoCmsModule`: apollo-angular ya no expone - * `ApolloModule`, la configuración se hace con providers. - */ -export function providePiDatoCms(): EnvironmentProviders { - return makeEnvironmentProviders([ - { - provide: APOLLO_OPTIONS, - useFactory: (httpLink: HttpLink) => { - return { - cache: new InMemoryCache(), - link: httpLink.create({ - uri: datoCms, - headers: { - Authorization: environment.DATO_KEY, - } as any, - }), - }; - }, - deps: [HttpLink], - }, - ]); -} diff --git a/libs/ui/src/lib/util/theme/index.ts b/libs/ui/src/lib/util/theme/index.ts new file mode 100644 index 0000000..1b62ce4 --- /dev/null +++ b/libs/ui/src/lib/util/theme/index.ts @@ -0,0 +1 @@ +export * from './theme.service'; diff --git a/libs/ui/src/lib/util/theme/theme.service.spec.ts b/libs/ui/src/lib/util/theme/theme.service.spec.ts new file mode 100644 index 0000000..7581275 --- /dev/null +++ b/libs/ui/src/lib/util/theme/theme.service.spec.ts @@ -0,0 +1,60 @@ +import { TestBed } from '@angular/core/testing'; + +import { PiThemeService } from './theme.service'; + +describe('PiThemeService', () => { + afterEach(() => { + localStorage.clear(); + document.body.className = ''; + document.documentElement.classList.remove('dark-mode'); + }); + + it('applies `pi-shell pi-shell__default` to by default', () => { + TestBed.inject(PiThemeService); + TestBed.flushEffects(); + + expect(document.body.className).toBe('pi-shell pi-shell__default'); + }); + + it('updates when the theme changes', () => { + const service = TestBed.inject(PiThemeService); + + service.setTheme('alternative'); + TestBed.flushEffects(); + + expect(document.body.className).toBe('pi-shell pi-shell__alternative'); + }); + + it('toggles `dark-mode` on ', () => { + const service = TestBed.inject(PiThemeService); + + service.setDarkMode(true); + TestBed.flushEffects(); + expect(document.documentElement.classList.contains('dark-mode')).toBe(true); + + service.toggleDarkMode(); + TestBed.flushEffects(); + expect(document.documentElement.classList.contains('dark-mode')).toBe(false); + }); + + it('persists the theme and dark mode across a fresh instance (reload)', () => { + const service = TestBed.inject(PiThemeService); + service.setTheme('minimal'); + service.setDarkMode(true); + TestBed.flushEffects(); + + TestBed.resetTestingModule(); + const reloaded = TestBed.inject(PiThemeService); + + expect(reloaded.theme()).toBe('minimal'); + expect(reloaded.darkMode()).toBe(true); + }); + + it('falls back to the default theme when nothing valid is stored', () => { + localStorage.setItem('pi-theme', JSON.stringify('not-a-real-theme')); + + const service = TestBed.inject(PiThemeService); + + expect(service.theme()).toBe('default'); + }); +}); diff --git a/libs/ui/src/lib/util/theme/theme.service.ts b/libs/ui/src/lib/util/theme/theme.service.ts new file mode 100644 index 0000000..7664463 --- /dev/null +++ b/libs/ui/src/lib/util/theme/theme.service.ts @@ -0,0 +1,98 @@ +import { DOCUMENT, isPlatformBrowser } from '@angular/common'; +import { + Injectable, + PLATFORM_ID, + computed, + effect, + inject, + signal, +} from '@angular/core'; + +import { ShellClass } from '../../styles'; +import { LocalStorageService } from '../local-storage'; + +export type ThemeVariant = 'default' | 'alternative' | 'minimal' | 'gradient'; + +const THEME_STORAGE_KEY = 'pi-theme'; +const DARK_MODE_STORAGE_KEY = 'pi-dark-mode'; + +const shellClassByVariant: Record = { + default: ShellClass.Default, + alternative: ShellClass.Alternative, + minimal: ShellClass.Minimal, + gradient: ShellClass.Gradient, +}; + +/** + * Centraliza lo que hasta ahora cada app reimplementaba a mano (ver el + * `app.ts` del playground antes de esta extracción): fijar + * `pi-shell pi-shell__` en `` — los overlays de Material (diálogo, + * snackbar) cuelgan fuera de `` y también necesitan heredarlo — y + * `dark-mode` en ``. + * + * `theme`/`darkMode` son señales normales: se pueden mutar tanto con + * `setTheme()`/`toggleDarkMode()` como con un `[(checked)]`/`[(value)]` dos + * vías directo sobre la señal. La aplicación de clases y la persistencia en + * `localStorage` viven en `effect()`, no en los setters, para que cualquier + * camino de escritura quede cubierto por igual. + * + * El modo oscuro en sí (qué tokens remapea `.dark-mode`) lo sigue definiendo + * cada app consumidora — este servicio solo pone o quita la clase. + */ +@Injectable({ providedIn: 'root' }) +export class PiThemeService { + private readonly document = inject(DOCUMENT); + private readonly platformId = inject(PLATFORM_ID); + private readonly localStorage = inject(LocalStorageService); + + readonly theme = signal(this.readStoredTheme()); + readonly darkMode = signal(this.readStoredDarkMode()); + + readonly shellClass = computed(() => shellClassByVariant[this.theme()]); + + constructor() { + effect(() => { + this.document.body.className = this.shellClass(); + this.persist(THEME_STORAGE_KEY, this.theme()); + }); + + effect(() => { + this.document.documentElement.classList.toggle( + 'dark-mode', + this.darkMode(), + ); + this.persist(DARK_MODE_STORAGE_KEY, this.darkMode()); + }); + } + + setTheme(theme: ThemeVariant): void { + this.theme.set(theme); + } + + setDarkMode(darkMode: boolean): void { + this.darkMode.set(darkMode); + } + + toggleDarkMode(): void { + this.darkMode.update((value) => !value); + } + + private persist(key: string, value: unknown): void { + if (isPlatformBrowser(this.platformId)) { + this.localStorage.setItem(key, value); + } + } + + private readStoredTheme(): ThemeVariant { + if (!isPlatformBrowser(this.platformId)) return 'default'; + const stored = this.localStorage.getItem(THEME_STORAGE_KEY); + return stored && stored in shellClassByVariant ? stored : 'default'; + } + + private readStoredDarkMode(): boolean { + return ( + isPlatformBrowser(this.platformId) && + !!this.localStorage.getItem(DARK_MODE_STORAGE_KEY) + ); + } +} diff --git a/libs/ui/tsconfig.generators.json b/libs/ui/tsconfig.generators.json new file mode 100644 index 0000000..43199d6 --- /dev/null +++ b/libs/ui/tsconfig.generators.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "module": "nodenext", + "moduleResolution": "nodenext", + "target": "es2021", + "outDir": "../../dist/libs/ui/generators", + "rootDir": "src/generators", + "declaration": false, + "sourceMap": false, + "types": ["node"] + }, + "include": ["src/generators/**/*.cts"] +} diff --git a/package-lock.json b/package-lock.json index 7914312..535be16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,13 +22,10 @@ "@angular/platform-server": "22.0.6", "@angular/router": "22.0.6", "@angular/ssr": "22.0.6", - "@apollo/client": "^4.2.8", "@ngx-translate/core": "^18.0.0", "@ngx-translate/http-loader": "^18.0.0", - "apollo-angular": "^14.1.0", "chart.js": "^4.5.1", "express": "^4.21.2", - "graphql": "^17.0.2", "lodash": "^4.18.1", "luxon": "^3.7.2", "ngx-infinite-scroll": "^22.0.0", @@ -90,7 +87,9 @@ "ng-packagr": "~22.0.0", "nx": "23.1.0", "postcss": "^8.4.5", + "postcss-scss": "^4.0.9", "prettier": "^3.8.1", + "stylelint": "^17.14.1", "supertest": "^7.1.4", "ts-jest": "^29.4.7", "ts-node": "10.9.1", @@ -2960,43 +2959,6 @@ } } }, - "node_modules/@apollo/client": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@apollo/client/-/client-4.2.8.tgz", - "integrity": "sha512-Iw/e/5fpOx+/2CgtcFNLdwxtdPzwTfmD4mYRjHA1UBdkfe6WOtWLRQeRESCUJCh49blXwp9I496A0MIoLgmCsw==", - "license": "MIT", - "dependencies": { - "@graphql-typed-document-node/core": "^3.1.1", - "@wry/caches": "^1.0.0", - "@wry/equality": "^0.5.6", - "@wry/trie": "^0.5.0", - "graphql-tag": "^2.12.6", - "optimism": "^0.18.0", - "tslib": "^2.3.0" - }, - "peerDependencies": { - "graphql": "^16.0.0 || ^17.0.0", - "graphql-ws": "^5.5.5 || ^6.0.3", - "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc", - "react-dom": "^17.0.0 || ^18.0.0 || >=19.0.0-rc", - "rxjs": "^7.3.0", - "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" - }, - "peerDependenciesMeta": { - "graphql-ws": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "subscriptions-transport-ws": { - "optional": true - } - } - }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -4974,6 +4936,50 @@ "dev": true, "license": "(Apache-2.0 AND BSD-3-Clause)" }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/memory/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, "node_modules/@colordx/core": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@colordx/core/-/core-5.5.0.tgz", @@ -5100,6 +5106,31 @@ "@csstools/css-tokenizer": "^3.0.4" } }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", + "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, "node_modules/@csstools/css-tokenizer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", @@ -5120,6 +5151,76 @@ "node": ">=18" } }, + "node_modules/@csstools/media-query-list-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-5.0.0.tgz", + "integrity": "sha512-T9lXmZOfnam3eMERPsszjY5NK0jX8RmThmmm99FZ8b7z8yMaFZWKwLWGZuTwdO3ddRY5fy13GmmEYZXB4I98Eg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/selector-resolve-nested": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-4.0.1.tgz", + "integrity": "sha512-j3vdQu0XwLME5qOTWxm8cnmvsf423R2YL6DbKklCHZwkDm7UdKNu6RPlw4REIJhSlKBICY3B70/7QZdicLqZgg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, + "node_modules/@csstools/selector-specificity": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-6.0.0.tgz", + "integrity": "sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.1.1" + } + }, "node_modules/@discoveryjs/json-ext": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-1.1.0.tgz", @@ -5875,15 +5976,6 @@ "node": "^20.17.0 || >=22.9.0" } }, - "node_modules/@graphql-typed-document-node/core": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", - "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", - "license": "MIT", - "peerDependencies": { - "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/@harperfast/extended-iterable": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@harperfast/extended-iterable/-/extended-iterable-1.0.3.tgz", @@ -8309,6 +8401,30 @@ "tslib": "2" } }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@kurkle/color": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", @@ -13391,6 +13507,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -15217,54 +15346,6 @@ "@xtuc/long": "4.2.2" } }, - "node_modules/@wry/caches": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", - "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wry/context": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.4.tgz", - "integrity": "sha512-jmT7Sb4ZQWI5iyu3lobQxICu2nC/vbUhP0vIdd6tHC9PTfenmRmuIFqktc6GH9cgi+ZHnsLWPvfSvc4DrYmKiQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wry/equality": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.5.7.tgz", - "integrity": "sha512-BRFORjsTuQv5gxcXsuDXx6oGRhuVsEGwZy6LOzRRfgu+eSfxbhUQ9L9YtSEIuIjY/o7g3iWFjrc5eSY1GXP2Dw==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@wry/trie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@wry/trie/-/trie-0.5.0.tgz", - "integrity": "sha512-FNoYzHawTMk/6KMQoEG5O4PuioX19UbwdQKF44yw0nLfOypfQdjtfZzo/UIJWAJ23sNIFbD1Ug9lbaDGMwbqQA==", - "license": "MIT", - "dependencies": { - "tslib": "^2.3.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", @@ -15596,24 +15677,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/apollo-angular": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/apollo-angular/-/apollo-angular-14.1.0.tgz", - "integrity": "sha512-rE6gcY9pSsI6FuG0tNhoVGxVJ82qN6vQAtG0t2yfujWTJRW1YD6aZe85D5LyyADM1oXGYySuYQcTtWBqAAQ6XQ==", - "license": "MIT", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16" - }, - "peerDependencies": { - "@angular/core": "^20.0.0 || ^21.0.0 || ^22.0.0", - "@apollo/client": "^4.0.1", - "graphql": "^16.0.0", - "rxjs": "^7.8.0" - } - }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -15693,6 +15756,16 @@ "dev": true, "license": "MIT" }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -16415,6 +16488,30 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/cacheable/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -16835,6 +16932,13 @@ "dev": true, "license": "MIT" }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true, + "license": "MIT" + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -17175,6 +17279,16 @@ "postcss": "^8.0.9" } }, + "node_modules/css-functions-list": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.3.3.tgz", + "integrity": "sha512-8HFEBPKhOpJPEPu70wJJetjKta86Gw9+CCyCnB3sui2qQfOvRyqBy4IKLKKAwdMpWb2lHXWk9Wb4Z6AmaUT1Pg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/css-loader": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", @@ -18791,6 +18905,16 @@ "fast-string-width": "^3.0.2" } }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", @@ -19592,6 +19716,54 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "17.8.0", "resolved": "https://registry.npmjs.org/globals/-/globals-17.8.0.tgz", @@ -19605,6 +19777,47 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-16.2.2.tgz", + "integrity": "sha512-NLvV9ubZ6NDsJaOpKPy3cQeJpKi9DcWiyCiFUpJPA0YihRqiE6RWaLUmgNNPr8MgPpLZjnBjSmou7uZBRJv9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "fast-glob": "^3.3.3", + "ignore": "^7.0.5", + "is-path-inside": "^4.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true, + "license": "MIT" + }, "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", @@ -19630,30 +19843,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/graphql": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-17.0.2.tgz", - "integrity": "sha512-FRWbddMxfkjiB7z+aQDWIR+E34xo9I8c9mtK2RPv8PmMzKRvrdsreHL/Ui/TmwHJfhHChEtsFPyMHKI+xuarQQ==", - "license": "MIT", - "engines": { - "node": "^22.0.0 || ^24.0.0 || ^25.0.0 || >=26.0.0" - } - }, - "node_modules/graphql-tag": { - "version": "2.12.7", - "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.7.tgz", - "integrity": "sha512-xnE/NFzy+0eIesvAsREJZ284zTl/wYuBAvpsFSDhRGRdRHdnE90M21Q3xAWyYInb0J756c6x0pIQ62+vtvOs1Q==", - "license": "MIT", - "dependencies": { - "tslib": "^2.1.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "graphql": "^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" - } - }, "node_modules/handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -19736,6 +19925,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/hasown": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -19768,6 +19970,13 @@ "node": ">=16.9.0" } }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", @@ -19853,6 +20062,19 @@ "dev": true, "license": "MIT" }, + "node_modules/html-tags": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-5.1.0.tgz", + "integrity": "sha512-n6l5uca7/y5joxZ3LUePhzmBFUJ+U2YWzhMa8XUTecSeSlQiZdF5XAd/Q3/WUl0VsXgUwWi8I7CNIwdI5WN1SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/htmlparser2": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", @@ -20201,6 +20423,17 @@ "node": ">=8" } }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -20444,6 +20677,19 @@ "node": ">=0.12.0" } }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-plain-obj": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", @@ -24451,6 +24697,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", @@ -24734,6 +24987,17 @@ "node": ">= 0.4" } }, + "node_modules/mathml-tag-names": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-4.0.0.tgz", + "integrity": "sha512-aa6AU2Pcx0VP/XWnh8IGL0SYSgQHDT6Ucror2j2mXeFAlN3ahaNs8EZtG1YiticMkSLj3Gt6VPFfZogt7G5iFQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/mdn-data": { "version": "2.27.1", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", @@ -24763,6 +25027,19 @@ "node": ">= 4.0.0" } }, + "node_modules/meow": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-14.1.0.tgz", + "integrity": "sha512-EDYo6VlmtnumlcBCbh1gLJ//9jvM/ndXHfVXIFrZVr6fGcwTUyCTFNTLCKuY3ffbK8L/+3Mzqnd58RojiZqHVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-descriptors": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", @@ -25148,9 +25425,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", + "version": "3.3.17", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz", + "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==", "funding": [ { "type": "github", @@ -26588,18 +26865,6 @@ "opener": "bin/opener-bin.js" } }, - "node_modules/optimism": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.18.1.tgz", - "integrity": "sha512-mLXNwWPa9dgFyDqkNi54sjDyNJ9/fTI6WGBLgnXku1vdKY/jovHfZT5r+aiVeFFLOz+foPNOm5YJ4mqgld2GBQ==", - "license": "MIT", - "dependencies": { - "@wry/caches": "^1.0.0", - "@wry/context": "^0.7.0", - "@wry/trie": "^0.5.0", - "tslib": "^2.3.0" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -27974,6 +28239,33 @@ "postcss": "^8.4.31" } }, + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" + } + }, "node_modules/postcss-selector-parser": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.4.tgz", @@ -28184,6 +28476,26 @@ "node": ">=16.0.0" } }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, "node_modules/qs": { "version": "6.15.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz", @@ -30168,6 +30480,293 @@ "postcss": "^8.5.13" } }, + "node_modules/stylelint": { + "version": "17.14.1", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-17.14.1.tgz", + "integrity": "sha512-xVQwyiuxALUBNB2fBe0tmNemg9KqLtdj3T64mioFDar79B2cU8LIyz+3KL6LdiHs9NkeNfwxpKSaIVOY8f112g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.2.1", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-syntax-patches-for-csstree": "^1.1.6", + "@csstools/css-tokenizer": "^4.0.0", + "@csstools/media-query-list-parser": "^5.0.0", + "@csstools/selector-resolve-nested": "^4.0.0", + "@csstools/selector-specificity": "^6.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^9.0.2", + "css-functions-list": "^3.3.3", + "css-tree": "^3.2.1", + "debug": "^4.4.3", + "fast-glob": "^3.3.3", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^11.1.5", + "global-modules": "^2.0.0", + "globby": "^16.2.1", + "globjoin": "^0.1.4", + "html-tags": "^5.1.0", + "ignore": "^7.0.5", + "import-meta-resolve": "^4.2.0", + "mathml-tag-names": "^4.0.0", + "meow": "^14.1.0", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.5.16", + "postcss-safe-parser": "^7.0.1", + "postcss-selector-parser": "^7.1.4", + "postcss-value-parser": "^4.2.0", + "string-width": "^8.2.1", + "supports-hyperlinks": "^4.5.0", + "svg-tags": "^1.0.0", + "table": "^6.9.0", + "write-file-atomic": "^7.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/stylelint/node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/stylelint/node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/stylelint/node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/stylelint/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/stylelint/node_modules/cosmiconfig": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/stylelint/node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/stylelint/node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/stylelint/node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/stylelint/node_modules/string-width": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz", + "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylelint/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/stylelint/node_modules/write-file-atomic": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-7.0.1.tgz", + "integrity": "sha512-OTIk8iR8/aCRWBqvxrzxR0hgxWpnYBblY1S5hDWBQfk/VFmJwzmJgQFN3WsoUKHISv2eAwe+PpbUzyL1CKTLXg==", + "dev": true, + "license": "ISC", + "dependencies": { + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/superagent": { "version": "10.3.0", "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz", @@ -30240,6 +30839,49 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.5.0.tgz", + "integrity": "sha512-ZW2OvfeCXrNTbLakPUzjQG922EeGCOteFSVoek5DKStTh898wf7zgtuFlzQN8HfZCxC3Eh02yJVrRW51hADf+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^5.0.1", + "supports-color": "^10.2.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-5.0.1.tgz", + "integrity": "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -30252,6 +30894,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, "node_modules/svgo": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.2.tgz", @@ -30354,6 +31002,56 @@ "url": "https://opencollective.com/synckit" } }, + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/table/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/tapable": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", @@ -31218,6 +31916,19 @@ "node": ">=4" } }, + "node_modules/unicorn-magic": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.4.0.tgz", + "integrity": "sha512-wH590V9VNgYH9g3lH9wWjTrUoKsjLF6sGLjhR4sH1LWpLmCOH0Zf7PukhDA8BiS7KHe4oPNkcTHqYkj7SOGUOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/union": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", diff --git a/package.json b/package.json index bce978b..ae41f53 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,10 @@ "@angular/platform-server": "22.0.6", "@angular/router": "22.0.6", "@angular/ssr": "22.0.6", - "@apollo/client": "^4.2.8", "@ngx-translate/core": "^18.0.0", "@ngx-translate/http-loader": "^18.0.0", - "apollo-angular": "^14.1.0", "chart.js": "^4.5.1", "express": "^4.21.2", - "graphql": "^17.0.2", "lodash": "^4.18.1", "luxon": "^3.7.2", "ngx-infinite-scroll": "^22.0.0", @@ -97,7 +94,9 @@ "ng-packagr": "~22.0.0", "nx": "23.1.0", "postcss": "^8.4.5", + "postcss-scss": "^4.0.9", "prettier": "^3.8.1", + "stylelint": "^17.14.1", "supertest": "^7.1.4", "ts-jest": "^29.4.7", "ts-node": "10.9.1", diff --git a/tools/stylelint-plugin-pi-theme/no-raw-color-token.mjs b/tools/stylelint-plugin-pi-theme/no-raw-color-token.mjs new file mode 100644 index 0000000..cd22846 --- /dev/null +++ b/tools/stylelint-plugin-pi-theme/no-raw-color-token.mjs @@ -0,0 +1,98 @@ +import stylelint from 'stylelint'; + +const { createPlugin, utils } = stylelint; +const { report, ruleMessages, validateOptions } = utils; + +const ruleName = 'pi/no-raw-color-token'; + +const messages = ruleMessages(ruleName, { + rejected: (token) => + `"${token}" se usa directamente. Los componentes de la librería deben ` + + `consumir un token --pi-theme-* con este color como fallback ` + + `(var(--pi-theme-, ${token})) para que un tema o el modo oscuro ` + + `puedan redefinir la superficie sin tocar el componente. Ver TOKENS.md.`, +}); + +const meta = { + url: 'https://github.com/pi/ui/tools/stylelint-plugin-pi-theme', +}; + +/** + * Contrato de esta librería (ver TOKENS.md): un componente NO consume + * `--pi-color-*` en crudo, lo hace a través de un `--pi-theme-*` con el color + * como fallback: `var(--pi-theme-x, var(--pi-color-y))`. Esta regla detecta + * el caso contrario — `--pi-color-*` referenciado fuera de cualquier + * fallback de `--pi-theme-*` — que es exactamente la clase de bug que dejó + * colores de marca fijos filtrando en modo oscuro (accordion/tab/select en + * esta misma librería, antes de tokenizarlos). + * + * Deliberadamente NO seguimos con `postcss-value-parser`: los valores aquí + * son casi siempre `var(...)` con como mucho una función envolvente + * (`linear-gradient(...)`, `rgba(...)`), así que un escaneo con paréntesis + * balanceados es suficiente y evita añadir una dependencia más solo para + * esta regla. + */ +function findMatchingParenEnd(value, openParenIndex) { + let depth = 1; + for (let i = openParenIndex + 1; i < value.length; i++) { + if (value[i] === '(') depth++; + else if (value[i] === ')') { + depth--; + if (depth === 0) return i; + } + } + return value.length - 1; +} + +/** Rangos `[start, end)` de cada fallback de un `var(--pi-theme-*, ...)` — lo que haya dentro está exento. */ +function findProtectedRanges(value) { + const ranges = []; + const pattern = /var\(\s*--pi-theme-[\w-]+/g; + let match; + while ((match = pattern.exec(value))) { + const openParenIndex = match.index + match[0].indexOf('('); + const end = findMatchingParenEnd(value, openParenIndex); + ranges.push([match.index, end]); + } + return ranges; +} + +function isProtected(index, ranges) { + return ranges.some(([start, end]) => index > start && index < end); +} + +const rule = (primaryOption) => { + return (root, result) => { + const validOptions = validateOptions(result, ruleName, { + actual: primaryOption, + possible: [true, false], + }); + if (!validOptions || !primaryOption) return; + + root.walkDecls((decl) => { + const value = decl.value; + if (!value || !value.includes('--pi-color-')) return; + + const protectedRanges = findProtectedRanges(value); + const colorPattern = /--pi-color-[\w-]+/g; + let match; + while ((match = colorPattern.exec(value))) { + if (isProtected(match.index, protectedRanges)) continue; + + report({ + message: messages.rejected(match[0]), + node: decl, + word: match[0], + result, + ruleName, + }); + } + }); + }; +}; + +rule.ruleName = ruleName; +rule.messages = messages; +rule.meta = meta; + +export default createPlugin(ruleName, rule);