prepara update
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,6 +8,9 @@ out-tsc
|
|||||||
# dependencies
|
# dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
|
||||||
|
# Credenciales de registro npm (Nexus u otro). Ver .npmrc.example.
|
||||||
|
.npmrc
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
/.idea
|
/.idea
|
||||||
.project
|
.project
|
||||||
|
|||||||
21
.npmrc.example
Normal file
21
.npmrc.example
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Copia este fichero a `.npmrc` (en la raíz del repo) y rellena los valores
|
||||||
|
# reales de tu Nexus. `.npmrc` está en .gitignore: nunca se commitea un token.
|
||||||
|
#
|
||||||
|
# Solo hace falta esto para PUBLICAR @pi/ui. Instalar paquetes públicos
|
||||||
|
# (@angular/*, rxjs, etc.) sigue yendo contra el registro público de npm salvo
|
||||||
|
# que tu Nexus también actúe de proxy/group para el scope por defecto.
|
||||||
|
|
||||||
|
# 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/
|
||||||
|
|
||||||
|
# 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/`
|
||||||
|
# (esto último escribe la línea de abajo automáticamente en tu ~/.npmrc).
|
||||||
|
//nexus.tuempresa.com/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
|
||||||
|
# "restricted" es lo correcto (no lo confundas con "hazlo público en npmjs").
|
||||||
|
always-auth=true
|
||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||||
import { provideRouter, withInMemoryScrolling } from '@angular/router';
|
import { provideRouter, withInMemoryScrolling } from '@angular/router';
|
||||||
import { provideStore } from '@ngrx/store';
|
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import {
|
import {
|
||||||
@@ -16,7 +15,7 @@ import {
|
|||||||
} from '@ngx-translate/core';
|
} from '@ngx-translate/core';
|
||||||
import { Observable, of } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
|
|
||||||
import { IconRegistryService, deviceConfigsReducer, featureKey } from '@pi/ui';
|
import { DeviceConfigsService, IconRegistryService } from '@pi/ui';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Diccionario en línea: la librería resuelve los textos con `| translate`, y
|
* Diccionario en línea: la librería resuelve los textos con `| translate`, y
|
||||||
@@ -62,12 +61,15 @@ export const appConfig: ApplicationConfig = {
|
|||||||
// Los iconos son SVG registrados en el MatIconRegistry (assets/icons).
|
// Los iconos son SVG registrados en el MatIconRegistry (assets/icons).
|
||||||
provideAppInitializer(() => inject(IconRegistryService).init()),
|
provideAppInitializer(() => inject(IconRegistryService).init()),
|
||||||
|
|
||||||
|
// data-table, carousel y background-image leen el tipo de dispositivo;
|
||||||
|
// instanciarlo arranca el listener de breakpoints.
|
||||||
|
provideAppInitializer(() => {
|
||||||
|
inject(DeviceConfigsService);
|
||||||
|
}),
|
||||||
|
|
||||||
provideTranslateService({
|
provideTranslateService({
|
||||||
fallbackLang: 'es',
|
fallbackLang: 'es',
|
||||||
loader: { provide: TranslateLoader, useClass: PlaygroundTranslateLoader },
|
loader: { provide: TranslateLoader, useClass: PlaygroundTranslateLoader },
|
||||||
}),
|
}),
|
||||||
|
|
||||||
// data-table y background-image leen el tipo de dispositivo del store.
|
|
||||||
provideStore({ [featureKey]: deviceConfigsReducer }),
|
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,10 +9,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="theme-toggles">
|
<div class="theme-toggles">
|
||||||
<label class="theme-toggle">
|
<div class="theme-switcher" role="radiogroup" aria-label="Tema">
|
||||||
<span>Tema alternativo</span>
|
@for (option of themeOptions; track option.value) {
|
||||||
<pi-slide-toggle [(checked)]="alternativeTheme" />
|
<button
|
||||||
</label>
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
[attr.aria-checked]="theme() === option.value"
|
||||||
|
[class.is-active]="theme() === option.value"
|
||||||
|
(click)="theme.set(option.value)"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<label class="theme-toggle">
|
<label class="theme-toggle">
|
||||||
<span>Modo oscuro</span>
|
<span>Modo oscuro</span>
|
||||||
<pi-slide-toggle [(checked)]="darkMode" />
|
<pi-slide-toggle [(checked)]="darkMode" />
|
||||||
@@ -32,16 +41,14 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<p class="theme-hint">
|
<p class="theme-hint">
|
||||||
Tema:
|
Tema: <strong>{{ theme() }}</strong> ·
|
||||||
<strong>{{ alternativeTheme() ? 'alternative' : 'default' }}</strong> ·
|
<code>{{ shellClassByTheme[theme()] }}</code>
|
||||||
<code
|
en el <code><body></code>. Los cuatro temas comparten exactamente los
|
||||||
>{{ alternativeTheme() ? 'pi-shell__alternative' : 'pi-shell__default'
|
|
||||||
}}</code
|
|
||||||
>
|
|
||||||
en el <code><body></code>. Los dos temas comparten exactamente los
|
|
||||||
mismos selectores: solo cambian los tokens <code>--pi-color-*</code> y
|
mismos selectores: solo cambian los tokens <code>--pi-color-*</code> y
|
||||||
<code>--pi-theme-*</code> que definen <code>themes/_default.scss</code> y
|
<code>--pi-theme-*</code> que definen <code>themes/_default.scss</code>,
|
||||||
<code>themes/_alternative.scss</code>.
|
<code>themes/_alternative.scss</code>, <code>themes/_minimal.scss</code> y
|
||||||
|
<code>themes/_gradient.scss</code>. Prueba "Gradient" y enfoca un campo del
|
||||||
|
formulario.
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,30 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.theme-switcher {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.25rem;
|
||||||
|
padding: 0.2rem;
|
||||||
|
border: 1px solid var(--pg-card-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.3rem 0.75rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--pg-text-muted);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background: var(--pg-badge-bg);
|
||||||
|
color: var(--pg-badge-text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.theme-hint {
|
.theme-hint {
|
||||||
margin: 0.75rem 0 0;
|
margin: 0.75rem 0 0;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
|||||||
@@ -33,13 +33,30 @@ export class App {
|
|||||||
|
|
||||||
protected readonly darkMode = signal(false);
|
protected readonly darkMode = signal(false);
|
||||||
|
|
||||||
|
protected readonly themeOptions: ReadonlyArray<{
|
||||||
|
value: ThemeVariant;
|
||||||
|
label: string;
|
||||||
|
}> = [
|
||||||
|
{ value: 'default', label: 'Default' },
|
||||||
|
{ value: 'alternative', label: 'Alternative' },
|
||||||
|
{ value: 'minimal', label: 'Minimal' },
|
||||||
|
{ value: 'gradient', label: 'Gradient' },
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tema visual de la librería. `ShellClass` devuelve las dos clases que hay
|
* Tema visual de la librería. `ShellClass` devuelve las dos clases que hay
|
||||||
* que poner juntas (`pi-shell pi-shell__<tema>`); van en el `<body>` porque
|
* que poner juntas (`pi-shell pi-shell__<tema>`); van en el `<body>` porque
|
||||||
* los overlays de Material (diálogos, snackbars) se montan fuera de
|
* los overlays de Material (diálogos, snackbars) se montan fuera de
|
||||||
* `<app-root>` y también deben heredarlas.
|
* `<app-root>` y también deben heredarlas.
|
||||||
*/
|
*/
|
||||||
protected readonly alternativeTheme = signal(false);
|
protected readonly theme = signal<ThemeVariant>('default');
|
||||||
|
|
||||||
|
protected readonly shellClassByTheme: Record<ThemeVariant, string> = {
|
||||||
|
default: ShellClass.Default,
|
||||||
|
alternative: ShellClass.Alternative,
|
||||||
|
minimal: ShellClass.Minimal,
|
||||||
|
gradient: ShellClass.Gradient,
|
||||||
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
@@ -50,10 +67,9 @@ export class App {
|
|||||||
});
|
});
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const shell = this.alternativeTheme()
|
this.document.body.className = this.shellClassByTheme[this.theme()];
|
||||||
? ShellClass.Alternative
|
|
||||||
: ShellClass.Default;
|
|
||||||
this.document.body.className = shell;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ThemeVariant = 'default' | 'alternative' | 'minimal' | 'gradient';
|
||||||
|
|||||||
@@ -195,6 +195,26 @@
|
|||||||
<pi-divider color="primary" width="large" label="Separador" />
|
<pi-divider color="primary" width="large" label="Separador" />
|
||||||
</app-section>
|
</app-section>
|
||||||
|
|
||||||
|
<app-section
|
||||||
|
title="Avatar y Badge"
|
||||||
|
hint="pi-avatar cae en cascada a iniciales (con name) y luego a icono; pi-badge envuelve cualquier contenido proyectado con un indicador superpuesto."
|
||||||
|
>
|
||||||
|
<pi-avatar image="https://i.pravatar.cc/128?img=12" alt="Ada Lovelace" />
|
||||||
|
<pi-avatar name="Ada Lovelace" color="primary" />
|
||||||
|
<pi-avatar name="Grace Hopper" color="accent" size="large" />
|
||||||
|
<pi-avatar color="highlighted" shape="square" />
|
||||||
|
|
||||||
|
<pi-badge [value]="3" color="warn">
|
||||||
|
<pi-icon-button icon="notification-on" />
|
||||||
|
</pi-badge>
|
||||||
|
<pi-badge color="success">
|
||||||
|
<pi-avatar name="Ada Lovelace" color="primary" />
|
||||||
|
</pi-badge>
|
||||||
|
<pi-badge [value]="128" [max]="99" color="primary" position="top-left">
|
||||||
|
<pi-icon-button icon="email" />
|
||||||
|
</pi-badge>
|
||||||
|
</app-section>
|
||||||
|
|
||||||
<app-section
|
<app-section
|
||||||
title="Cabecera y listado"
|
title="Cabecera y listado"
|
||||||
hint="pi-headline para portadas de sección, pi-list-item para filas de listado. pi-headline resuelve pathImg contra un CDN externo fijo, así que la ilustración solo carga con red."
|
hint="pi-headline para portadas de sección, pi-list-item para filas de listado. pi-headline resuelve pathImg contra un CDN externo fijo, así que la ilustración solo carga con red."
|
||||||
@@ -481,6 +501,49 @@
|
|||||||
</button>
|
</button>
|
||||||
</app-section>
|
</app-section>
|
||||||
|
|
||||||
|
<app-section
|
||||||
|
title="Drawer"
|
||||||
|
hint="pi-drawer tampoco se usa como etiqueta: se abre vía MatDialog con getDrawerConfigs, igual que pi-modal pero anclado a un borde."
|
||||||
|
>
|
||||||
|
<button pi-primary (click)="openDemoDrawer('right')">
|
||||||
|
Abrir drawer (derecha)
|
||||||
|
</button>
|
||||||
|
<button pi-outline (click)="openDemoDrawer('left')">
|
||||||
|
Abrir drawer (izquierda)
|
||||||
|
</button>
|
||||||
|
<ng-template #drawerTemplate>
|
||||||
|
<p>Contenido de ejemplo dentro del drawer.</p>
|
||||||
|
<p>Puede llevar cualquier plantilla: formularios, listas, filtros…</p>
|
||||||
|
</ng-template>
|
||||||
|
</app-section>
|
||||||
|
|
||||||
|
<app-section
|
||||||
|
title="Popover"
|
||||||
|
hint="pi-popover es una directiva ([pi-popover]) sobre @angular/cdk/overlay: no hay componente Material de contenido enriquecido que envolver."
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
pi-outline
|
||||||
|
pi-popover
|
||||||
|
[popoverContent]="popoverTemplate"
|
||||||
|
popoverPosition="below"
|
||||||
|
>
|
||||||
|
Click (popover)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
pi-outline
|
||||||
|
pi-popover
|
||||||
|
[popoverContent]="popoverTemplate"
|
||||||
|
popoverPosition="above"
|
||||||
|
popoverTrigger="hover"
|
||||||
|
>
|
||||||
|
Hover (popover)
|
||||||
|
</button>
|
||||||
|
<ng-template #popoverTemplate let-close="close">
|
||||||
|
<p>Contenido del popover, con cierre programático.</p>
|
||||||
|
<button pi-ghost (click)="close()">Cerrar</button>
|
||||||
|
</ng-template>
|
||||||
|
</app-section>
|
||||||
|
|
||||||
<app-section title="Data grid">
|
<app-section title="Data grid">
|
||||||
<div class="stack full">
|
<div class="stack full">
|
||||||
<pi-data-grid
|
<pi-data-grid
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
Component,
|
Component,
|
||||||
|
TemplateRef,
|
||||||
inject,
|
inject,
|
||||||
signal,
|
signal,
|
||||||
|
viewChild,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import {
|
||||||
FormField,
|
FormField,
|
||||||
@@ -19,6 +21,8 @@ import {
|
|||||||
PiAccordionItemContentDirective,
|
PiAccordionItemContentDirective,
|
||||||
PiAccordionItemDirective,
|
PiAccordionItemDirective,
|
||||||
PiActionButtonComponent,
|
PiActionButtonComponent,
|
||||||
|
PiAvatarComponent,
|
||||||
|
PiBadgeComponent,
|
||||||
PiBannerComponent,
|
PiBannerComponent,
|
||||||
PiBaseDataTableComponent,
|
PiBaseDataTableComponent,
|
||||||
PiCardBottomContentDirective,
|
PiCardBottomContentDirective,
|
||||||
@@ -46,6 +50,9 @@ import {
|
|||||||
DatasetColor,
|
DatasetColor,
|
||||||
Device,
|
Device,
|
||||||
PiDividerComponent,
|
PiDividerComponent,
|
||||||
|
DrawerConfigKeys,
|
||||||
|
PiDrawerComponent,
|
||||||
|
getDrawerConfigs,
|
||||||
PiEmailFieldComponent,
|
PiEmailFieldComponent,
|
||||||
PiExpansionPanelComponent,
|
PiExpansionPanelComponent,
|
||||||
PiGhostButtonDirective,
|
PiGhostButtonDirective,
|
||||||
@@ -83,6 +90,7 @@ import {
|
|||||||
PiPanelExpandableContentDirective,
|
PiPanelExpandableContentDirective,
|
||||||
PiPasswordFieldComponent,
|
PiPasswordFieldComponent,
|
||||||
PiPhoneFieldComponent,
|
PiPhoneFieldComponent,
|
||||||
|
PiPopoverDirective,
|
||||||
PiProgressBarComponent,
|
PiProgressBarComponent,
|
||||||
PiRadioButtonComponent,
|
PiRadioButtonComponent,
|
||||||
PiRange,
|
PiRange,
|
||||||
@@ -189,6 +197,8 @@ interface DemoModel {
|
|||||||
PiIconComponent,
|
PiIconComponent,
|
||||||
PiTextViewerComponent,
|
PiTextViewerComponent,
|
||||||
PiDividerComponent,
|
PiDividerComponent,
|
||||||
|
PiAvatarComponent,
|
||||||
|
PiBadgeComponent,
|
||||||
PiChipComponent,
|
PiChipComponent,
|
||||||
PiChipStatusComponent,
|
PiChipStatusComponent,
|
||||||
PiChipValueComponent,
|
PiChipValueComponent,
|
||||||
@@ -205,6 +215,7 @@ interface DemoModel {
|
|||||||
PiListItemRightContentDirective,
|
PiListItemRightContentDirective,
|
||||||
PiListItemRightIconDirective,
|
PiListItemRightIconDirective,
|
||||||
PiTooltipDirective,
|
PiTooltipDirective,
|
||||||
|
PiPopoverDirective,
|
||||||
PiNotFoundComponent,
|
PiNotFoundComponent,
|
||||||
// Feedback
|
// Feedback
|
||||||
PiProgressSpinnerComponent,
|
PiProgressSpinnerComponent,
|
||||||
@@ -497,6 +508,23 @@ export class CatalogPage {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Drawer -------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/** Igual que `pi-modal`: se abre desde TypeScript vía `MatDialog`. */
|
||||||
|
private readonly drawerTemplate =
|
||||||
|
viewChild.required<TemplateRef<unknown>>('drawerTemplate');
|
||||||
|
|
||||||
|
protected openDemoDrawer(side: 'left' | 'right'): void {
|
||||||
|
this.dialog.open(
|
||||||
|
PiDrawerComponent,
|
||||||
|
getDrawerConfigs(
|
||||||
|
side === 'left' ? DrawerConfigKeys.Left : DrawerConfigKeys.Right,
|
||||||
|
{ title: 'Detalle', template: this.drawerTemplate() },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
this.log(`drawer abierto (${side})`);
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Datos: grid, tabla, gráfico, carrusel ------------------------------ */
|
/* --- Datos: grid, tabla, gráfico, carrusel ------------------------------ */
|
||||||
|
|
||||||
protected readonly employees = [
|
protected readonly employees = [
|
||||||
|
|||||||
@@ -180,10 +180,9 @@ El origen tiene símbolos duplicados. En `index.ts` gana el primero y el otro se
|
|||||||
omite (queda anotado en el fichero). Siguen siendo accesibles por ruta directa:
|
omite (queda anotado en el fichero). Siguen siendo accesibles por ruta directa:
|
||||||
|
|
||||||
| Símbolo | Se exporta desde | Se omite de |
|
| Símbolo | Se exporta desde | Se omite de |
|
||||||
| ----------------------------------------------- | --------------------- | ------------------- |
|
| ------------------------- | ---------------- | ------------------ |
|
||||||
| `PiSlideToggleComponent` | `ui/form-field` | `ui/slide-toggle` |
|
| `PiSlideToggleComponent` | `ui/form-field` | `ui/slide-toggle` |
|
||||||
| `PiDisabledDirective` | `util/status` | `ui/text` |
|
| `PiDisabledDirective` | `util/status` | `ui/text` |
|
||||||
| `featureKey`, `getFeatureState`, `initialState` | `util/device-configs` | `util/list-configs` |
|
|
||||||
|
|
||||||
## APIs de terceros actualizadas
|
## APIs de terceros actualizadas
|
||||||
|
|
||||||
@@ -197,7 +196,6 @@ omite (queda anotado en el fichero). Siguen siendo accesibles por ruta directa:
|
|||||||
|
|
||||||
- **Tests**: no se copiaron los `*.spec.ts` del origen.
|
- **Tests**: no se copiaron los `*.spec.ts` del origen.
|
||||||
- **`Sector`** vivía en `deale-saas/util/models` (lib de aplicación, fuera de alcance); se trajo la interfaz a `util/models`.
|
- **`Sector`** vivía en `deale-saas/util/models` (lib de aplicación, fuera de alcance); se trajo la interfaz a `util/models`.
|
||||||
- **ngrx 21 con Angular 22**: instalado con `--legacy-peer-deps` (aún no declara soporte para 22).
|
|
||||||
- **Lint**: `nx lint ui` falla con avisos heredados del código original —
|
- **Lint**: `nx lint ui` falla con avisos heredados del código original —
|
||||||
`prefer-inject` (51), `directive-selector` (46), `no-explicit-any` (35),
|
`prefer-inject` (51), `directive-selector` (46), `no-explicit-any` (35),
|
||||||
`no-unused-expressions` (33), a11y (`click-events-have-key-events`,
|
`no-unused-expressions` (33), a11y (`click-events-have-key-events`,
|
||||||
|
|||||||
BIN
libs/ui/docs/theming.pdf
Normal file
BIN
libs/ui/docs/theming.pdf
Normal file
Binary file not shown.
@@ -23,6 +23,23 @@
|
|||||||
"development": {}
|
"development": {}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"package": {
|
||||||
|
"executor": "@nx/angular:package",
|
||||||
|
"outputs": [
|
||||||
|
"{workspaceRoot}/dist/{projectRoot}"
|
||||||
|
],
|
||||||
|
"defaultConfiguration": "production",
|
||||||
|
"options": {
|
||||||
|
"project": "libs/ui/ng-package.json",
|
||||||
|
"tsConfig": "libs/ui/tsconfig.lib.json"
|
||||||
|
},
|
||||||
|
"configurations": {
|
||||||
|
"production": {
|
||||||
|
"tsConfig": "libs/ui/tsconfig.lib.publish.json"
|
||||||
|
},
|
||||||
|
"development": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"executor": "@nx/jest:jest",
|
"executor": "@nx/jest:jest",
|
||||||
"outputs": [
|
"outputs": [
|
||||||
|
|||||||
@@ -11,16 +11,7 @@ export * from './lib/util/device-configs';
|
|||||||
export * from './lib/util/environments';
|
export * from './lib/util/environments';
|
||||||
export * from './lib/util/icon-registry';
|
export * from './lib/util/icon-registry';
|
||||||
export * from './lib/util/infinite-scroll';
|
export * from './lib/util/infinite-scroll';
|
||||||
// ./lib/util/list-configs: omitido(s) por colisión con otro módulo -> featureKey, getFeatureState, initialState
|
export * from './lib/util/list-configs';
|
||||||
export {
|
|
||||||
getListMode,
|
|
||||||
ListConfigsFacade,
|
|
||||||
listConfigsReducer,
|
|
||||||
ListModeActionsType,
|
|
||||||
resetListMode,
|
|
||||||
setListMode,
|
|
||||||
} from './lib/util/list-configs';
|
|
||||||
export type { ListConfigsState, ListMode } from './lib/util/list-configs';
|
|
||||||
export * from './lib/util/loaders';
|
export * from './lib/util/loaders';
|
||||||
export * from './lib/util/local-storage';
|
export * from './lib/util/local-storage';
|
||||||
export * from './lib/util/menu';
|
export * from './lib/util/menu';
|
||||||
@@ -36,6 +27,8 @@ export * from './lib/util/value';
|
|||||||
export * from './lib/util/view-title';
|
export * from './lib/util/view-title';
|
||||||
|
|
||||||
export * from './lib/ui/accordion';
|
export * from './lib/ui/accordion';
|
||||||
|
export * from './lib/ui/avatar';
|
||||||
|
export * from './lib/ui/badge';
|
||||||
export * from './lib/ui/banner';
|
export * from './lib/ui/banner';
|
||||||
export * from './lib/ui/button';
|
export * from './lib/ui/button';
|
||||||
export * from './lib/ui/card';
|
export * from './lib/ui/card';
|
||||||
@@ -45,6 +38,7 @@ export * from './lib/ui/chip';
|
|||||||
export * from './lib/ui/data-grid';
|
export * from './lib/ui/data-grid';
|
||||||
export * from './lib/ui/data-table';
|
export * from './lib/ui/data-table';
|
||||||
export * from './lib/ui/divider';
|
export * from './lib/ui/divider';
|
||||||
|
export * from './lib/ui/drawer';
|
||||||
export * from './lib/ui/error-status-code';
|
export * from './lib/ui/error-status-code';
|
||||||
export * from './lib/ui/file-upload';
|
export * from './lib/ui/file-upload';
|
||||||
export * from './lib/ui/form-field';
|
export * from './lib/ui/form-field';
|
||||||
@@ -57,6 +51,7 @@ export * from './lib/ui/note';
|
|||||||
export * from './lib/ui/notification';
|
export * from './lib/ui/notification';
|
||||||
export * from './lib/ui/paginator';
|
export * from './lib/ui/paginator';
|
||||||
export * from './lib/ui/panel';
|
export * from './lib/ui/panel';
|
||||||
|
export * from './lib/ui/popover';
|
||||||
export * from './lib/ui/progress';
|
export * from './lib/ui/progress';
|
||||||
export * from './lib/ui/select';
|
export * from './lib/ui/select';
|
||||||
export * from './lib/ui/slider';
|
export * from './lib/ui/slider';
|
||||||
|
|||||||
17
libs/ui/src/lib/styles/common/components/_pi-popover.scss
Normal file
17
libs/ui/src/lib/styles/common/components/_pi-popover.scss
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// El panel de `pi-popover` es un overlay de `@angular/cdk/overlay`: cuelga de
|
||||||
|
// `.cdk-overlay-container`, fuera del árbol del componente que lo dispara.
|
||||||
|
.cdk-overlay-container {
|
||||||
|
.pi-popover__panel {
|
||||||
|
// `.cdk-overlay-pane` ya es `display: flex` (así lo fija el propio CDK);
|
||||||
|
// el `<ng-template>` del consumidor no aporta un contenedor propio, así
|
||||||
|
// que sus hijos quedan como items flex directos de este panel. Sin
|
||||||
|
// `column` quedarían en fila en vez de apilados.
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--pi-spacing-x-small);
|
||||||
|
background-color: var(--pi-color-white);
|
||||||
|
border-radius: var(--pi-border-radius-medium);
|
||||||
|
box-shadow: var(--pi-box-shadow-panel);
|
||||||
|
padding: var(--pi-spacing-medium);
|
||||||
|
max-width: 20rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
@forward 'pi-list-item';
|
@forward 'pi-list-item';
|
||||||
@forward 'pi-note';
|
@forward 'pi-note';
|
||||||
@forward 'pi-panel';
|
@forward 'pi-panel';
|
||||||
|
@forward 'pi-popover';
|
||||||
@forward 'pi-spinner';
|
@forward 'pi-spinner';
|
||||||
@forward 'pi-tab';
|
@forward 'pi-tab';
|
||||||
@forward 'pi-text-viewer';
|
@forward 'pi-text-viewer';
|
||||||
|
|||||||
40
libs/ui/src/lib/styles/common/vendors/_mat-drawer.scss
vendored
Normal file
40
libs/ui/src/lib/styles/common/vendors/_mat-drawer.scss
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// El panel de `pi-drawer` es un `MatDialogConfig.panelClass`: el overlay lo
|
||||||
|
// cuelga fuera de `pi-drawer.component`, así que estos estilos no pueden ir
|
||||||
|
// en el scss del componente (view encapsulation no llegaría hasta ahí).
|
||||||
|
.cdk-overlay-container,
|
||||||
|
.pi-shell {
|
||||||
|
.pi-drawer__panel {
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
.mat-mdc-dialog-container,
|
||||||
|
.mdc-dialog__surface {
|
||||||
|
border-radius: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pi-drawer__panel--right {
|
||||||
|
animation: pi-drawer-slide-in-right 0.25s ease-out;
|
||||||
|
}
|
||||||
|
&.pi-drawer__panel--left {
|
||||||
|
animation: pi-drawer-slide-in-left 0.25s ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pi-drawer-slide-in-right {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pi-drawer-slide-in-left {
|
||||||
|
from {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,35 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Anillo de foco en degradado: opt-in por tema vía
|
||||||
|
// `--pi-theme-field-focus-gradient` (ver `themes/_gradient.scss`). Los
|
||||||
|
// temas que no la declaren pintan `background: none` y no se ve nada.
|
||||||
|
//
|
||||||
|
// El notched-outline dibuja su borde partido en 3 piezas (leading/notch/
|
||||||
|
// trailing) para dejar hueco a la etiqueta flotante; un degradado
|
||||||
|
// continuo ahí se vería cortado. Por eso el anillo va aparte, como
|
||||||
|
// `::after` de `.mat-mdc-text-field-wrapper` (la caja completa, sin
|
||||||
|
// partir), con `mask-composite` para que solo pinte el borde y no tape
|
||||||
|
// ni el texto ni los iconos.
|
||||||
|
.mat-mdc-text-field-wrapper {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
&.mat-focused .mat-mdc-text-field-wrapper::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
border-radius: var(--pi-theme-field-radius);
|
||||||
|
padding: var(--pi-theme-field-border-width, 1px);
|
||||||
|
background: var(--pi-theme-field-focus-gradient, none);
|
||||||
|
-webkit-mask:
|
||||||
|
linear-gradient(#fff 0 0) content-box,
|
||||||
|
linear-gradient(#fff 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
// NOT Invalid
|
// NOT Invalid
|
||||||
&:not(.mat-form-field-invalid) {
|
&:not(.mat-form-field-invalid) {
|
||||||
&:not(.mat-focused) {
|
&:not(.mat-focused) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
@forward 'mat-chip';
|
@forward 'mat-chip';
|
||||||
@forward 'mat-datepicker';
|
@forward 'mat-datepicker';
|
||||||
@forward 'mat-dialog';
|
@forward 'mat-dialog';
|
||||||
|
@forward 'mat-drawer';
|
||||||
@forward 'mat-expansion-panel';
|
@forward 'mat-expansion-panel';
|
||||||
@forward 'mat-form-field';
|
@forward 'mat-form-field';
|
||||||
@forward 'mat-menu';
|
@forward 'mat-menu';
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ export const ShellClass = {
|
|||||||
Default: shellTheming('default'),
|
Default: shellTheming('default'),
|
||||||
/** Aspecto de marketing: degradados, brillo al hover y controles mayores. */
|
/** Aspecto de marketing: degradados, brillo al hover y controles mayores. */
|
||||||
Alternative: shellTheming('alternative'),
|
Alternative: shellTheming('alternative'),
|
||||||
|
/** Esquinas casi rectas, borde fino en vez de sombra, sin degradados. */
|
||||||
|
Minimal: shellTheming('minimal'),
|
||||||
|
/** Como Default, pero con anillo en degradado al enfocar un campo. */
|
||||||
|
Gradient: shellTheming('gradient'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// View component level
|
// View component level
|
||||||
|
|||||||
49
libs/ui/src/lib/styles/themes/_gradient.scss
Normal file
49
libs/ui/src/lib/styles/themes/_gradient.scss
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// TEMA GRADIENT
|
||||||
|
//
|
||||||
|
// Como `_alternative.scss`, reusa el contrato de tokens sin tocar selectores
|
||||||
|
// de componente. La firma de este tema es el degradado: en el anillo de foco
|
||||||
|
// de los campos (`--pi-theme-field-focus-gradient`, consumido en
|
||||||
|
// `_mat-form-field.scss`) y en el relleno de los botones
|
||||||
|
// (`--pi-theme-button-fill-*`, consumido en `_pi-button-theme.scss`, el mismo
|
||||||
|
// mecanismo que ya usa `_alternative.scss`). El resto (forma, tamaño,
|
||||||
|
// tipografía) se deja igual que el tema por defecto para que el foco visual
|
||||||
|
// sea justo el degradado.
|
||||||
|
//
|
||||||
|
// `--pi-theme-field-border-color-focus: transparent` apaga el borde sólido
|
||||||
|
// del notched-outline al enfocar: si no, se vería el borde plano por debajo
|
||||||
|
// del anillo en degradado.
|
||||||
|
//
|
||||||
|
// `warn`/`success` no se tocan (igual que en `_alternative.scss`): son
|
||||||
|
// colores semánticos y un degradado ahí perjudicaría la lectura del estado.
|
||||||
|
|
||||||
|
@mixin gradientThemeTokens {
|
||||||
|
// --- Campos: anillo de foco en degradado ------------------------------
|
||||||
|
--pi-theme-field-focus-gradient: linear-gradient(
|
||||||
|
120deg,
|
||||||
|
var(--pi-color-primary),
|
||||||
|
var(--pi-color-accent),
|
||||||
|
var(--pi-color-highlighted)
|
||||||
|
);
|
||||||
|
--pi-theme-field-border-color-focus: transparent;
|
||||||
|
|
||||||
|
// --- Botones: relleno en degradado -------------------------------------
|
||||||
|
--pi-theme-button-fill-primary: linear-gradient(
|
||||||
|
120deg,
|
||||||
|
var(--pi-color-primary-400),
|
||||||
|
var(--pi-color-primary-900)
|
||||||
|
);
|
||||||
|
--pi-theme-button-fill-accent: linear-gradient(
|
||||||
|
120deg,
|
||||||
|
var(--pi-color-accent-400),
|
||||||
|
var(--pi-color-accent-900)
|
||||||
|
);
|
||||||
|
--pi-theme-button-fill-highlighted: linear-gradient(
|
||||||
|
120deg,
|
||||||
|
var(--pi-color-highlighted-300),
|
||||||
|
var(--pi-color-highlighted)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-shell__gradient {
|
||||||
|
@include gradientThemeTokens;
|
||||||
|
}
|
||||||
50
libs/ui/src/lib/styles/themes/_minimal.scss
Normal file
50
libs/ui/src/lib/styles/themes/_minimal.scss
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// TEMA MINIMALISTA
|
||||||
|
//
|
||||||
|
// Sale del lenguaje visual "Material" (relleno con degradado, esquinas muy
|
||||||
|
// redondeadas, sombra de elevación) hacia algo más plano: esquinas casi
|
||||||
|
// rectas, borde fino en vez de sombra, sin versalitas ni interletrado. Reusa
|
||||||
|
// el mismo contrato de tokens que `_alternative.scss`, así que tampoco
|
||||||
|
// duplica ni un selector de componente.
|
||||||
|
//
|
||||||
|
// La paleta de marca (--pi-color-*) se deja intacta a propósito: "minimalista"
|
||||||
|
// aquí es una cuestión de forma (radio, sombra, peso), no de color.
|
||||||
|
|
||||||
|
@mixin minimalThemeTokens {
|
||||||
|
// --- Botones: esquinas casi rectas, sin sombra ni degradado -----------
|
||||||
|
--pi-theme-button-height: 2.5rem;
|
||||||
|
--pi-theme-button-radius: var(--pi-border-radius-xx-small);
|
||||||
|
--pi-theme-button-font-weight: 500;
|
||||||
|
--pi-theme-button-letter-spacing: normal;
|
||||||
|
--pi-theme-button-text-transform: none;
|
||||||
|
--pi-theme-button-shadow: none;
|
||||||
|
--pi-theme-button-transition:
|
||||||
|
background-color 0.12s ease-out,
|
||||||
|
border-color 0.12s ease-out;
|
||||||
|
--pi-theme-button-fill-primary: none;
|
||||||
|
--pi-theme-button-fill-accent: none;
|
||||||
|
--pi-theme-button-fill-warn: none;
|
||||||
|
--pi-theme-button-fill-success: none;
|
||||||
|
--pi-theme-button-fill-highlighted: none;
|
||||||
|
|
||||||
|
// --- Campos: mismas esquinas rectas, borde fino y neutro --------------
|
||||||
|
--pi-theme-field-radius: var(--pi-border-radius-xx-small);
|
||||||
|
--pi-theme-field-border-width: 1px;
|
||||||
|
--pi-theme-field-border-color: var(--pi-color-grey-200);
|
||||||
|
--pi-theme-field-border-color-hover: var(--pi-color-grey-400);
|
||||||
|
--pi-theme-field-border-color-focus: var(--pi-color-black);
|
||||||
|
--pi-theme-field-surface: var(--pi-color-white);
|
||||||
|
--pi-theme-field-label-color: var(--pi-color-grey-400);
|
||||||
|
--pi-theme-field-label-transform: none;
|
||||||
|
--pi-theme-field-label-letter-spacing: normal;
|
||||||
|
--pi-theme-field-label-weight: 500;
|
||||||
|
|
||||||
|
// --- Superficies: separan por borde, no por elevación -----------------
|
||||||
|
--pi-theme-card-radius: var(--pi-border-radius-xx-small);
|
||||||
|
--pi-theme-card-shadow: none;
|
||||||
|
--pi-theme-panel-radius: var(--pi-border-radius-xx-small);
|
||||||
|
--pi-theme-chip-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-shell__minimal {
|
||||||
|
@include minimalThemeTokens;
|
||||||
|
}
|
||||||
@@ -1,7 +1,11 @@
|
|||||||
// El orden importa: `default` declara el contrato completo de tokens y
|
// El orden importa: `default` declara el contrato completo de tokens y los
|
||||||
// `alternative` lo sobreescribe.
|
// demás temas lo sobreescriben.
|
||||||
@forward 'default';
|
@forward 'default';
|
||||||
@forward 'alternative';
|
@forward 'alternative';
|
||||||
|
@forward 'minimal';
|
||||||
|
@forward 'gradient';
|
||||||
|
|
||||||
@use 'default' as *;
|
@use 'default' as *;
|
||||||
@use 'alternative' as *;
|
@use 'alternative' as *;
|
||||||
|
@use 'minimal' as *;
|
||||||
|
@use 'gradient' as *;
|
||||||
|
|||||||
7
libs/ui/src/lib/ui/avatar/avatar/avatar.component.html
Normal file
7
libs/ui/src/lib/ui/avatar/avatar/avatar.component.html
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
@if (showImage()) {
|
||||||
|
<img [src]="image()!" [alt]="alt() ?? name() ?? ''" (error)="onImageError()" />
|
||||||
|
} @else if (initials()) {
|
||||||
|
<span class="pi-avatar__initials">{{ initials() }}</span>
|
||||||
|
} @else {
|
||||||
|
<pi-icon [icon]="icon()" [size]="size()" />
|
||||||
|
}
|
||||||
73
libs/ui/src/lib/ui/avatar/avatar/avatar.component.scss
Normal file
73
libs/ui/src/lib/ui/avatar/avatar/avatar.component.scss
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
@mixin avatarColor($color) {
|
||||||
|
background-color: var(--pi-color-#{$color}-75);
|
||||||
|
color: var(--pi-color-#{$color});
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin avatarSize($dimension, $font-size) {
|
||||||
|
height: $dimension;
|
||||||
|
width: $dimension;
|
||||||
|
min-width: $dimension;
|
||||||
|
|
||||||
|
.pi-avatar__initials {
|
||||||
|
font-size: $font-size;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-avatar__initials {
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shape
|
||||||
|
&.pi-avatar__shape--circle {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
&.pi-avatar__shape--square {
|
||||||
|
border-radius: var(--pi-border-radius-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size
|
||||||
|
&.pi-avatar__size--x-small {
|
||||||
|
@include avatarSize(1.5rem, var(--pi-font-size-xxx-small));
|
||||||
|
}
|
||||||
|
&.pi-avatar__size--small {
|
||||||
|
@include avatarSize(2rem, var(--pi-font-size-xx-small));
|
||||||
|
}
|
||||||
|
&.pi-avatar__size--medium {
|
||||||
|
@include avatarSize(2.5rem, var(--pi-font-size-x-small));
|
||||||
|
}
|
||||||
|
&.pi-avatar__size--large {
|
||||||
|
@include avatarSize(3.25rem, var(--pi-font-size-small));
|
||||||
|
}
|
||||||
|
&.pi-avatar__size--x-large {
|
||||||
|
@include avatarSize(4rem, var(--pi-font-size-medium));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color (fallback: iniciales o icono)
|
||||||
|
&.pi-color__primary {
|
||||||
|
@include avatarColor(primary);
|
||||||
|
}
|
||||||
|
&.pi-color__accent {
|
||||||
|
@include avatarColor(accent);
|
||||||
|
}
|
||||||
|
&.pi-color__highlighted {
|
||||||
|
@include avatarColor(highlighted);
|
||||||
|
}
|
||||||
|
&.pi-color__grey {
|
||||||
|
@include avatarColor(grey);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
libs/ui/src/lib/ui/avatar/avatar/avatar.component.ts
Normal file
60
libs/ui/src/lib/ui/avatar/avatar/avatar.component.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
HostBinding,
|
||||||
|
computed,
|
||||||
|
input,
|
||||||
|
signal,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
// Shared
|
||||||
|
import { iconSize, theming } from '../../../styles';
|
||||||
|
import { PiIconComponent } from '../../icon/icon/icon.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pi-avatar',
|
||||||
|
templateUrl: './avatar.component.html',
|
||||||
|
styleUrls: ['./avatar.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [PiIconComponent],
|
||||||
|
})
|
||||||
|
export class PiAvatarComponent {
|
||||||
|
@HostBinding('class') get classes(): string {
|
||||||
|
return [
|
||||||
|
'pi-avatar',
|
||||||
|
`pi-avatar__size--${this.size()}`,
|
||||||
|
`pi-avatar__shape--${this.shape()}`,
|
||||||
|
`pi-color__${this.color()}`,
|
||||||
|
].join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Content
|
||||||
|
readonly image = input<string>();
|
||||||
|
readonly alt = input<string>();
|
||||||
|
readonly name = input<string>();
|
||||||
|
/** Icono de respaldo cuando no hay `image` ni `name`. */
|
||||||
|
readonly icon = input('user');
|
||||||
|
|
||||||
|
// Skin
|
||||||
|
readonly size = input<iconSize>('medium');
|
||||||
|
readonly shape = input<'circle' | 'square'>('circle');
|
||||||
|
readonly color = input<theming>('primary');
|
||||||
|
|
||||||
|
private readonly imageFailed = signal(false);
|
||||||
|
|
||||||
|
protected readonly showImage = computed(
|
||||||
|
() => !!this.image() && !this.imageFailed(),
|
||||||
|
);
|
||||||
|
|
||||||
|
protected readonly initials = computed(() => {
|
||||||
|
const value = this.name()?.trim();
|
||||||
|
if (!value) return '';
|
||||||
|
const [first, ...rest] = value.split(/\s+/).filter(Boolean);
|
||||||
|
const last = rest[rest.length - 1];
|
||||||
|
return ((first?.[0] ?? '') + (last?.[0] ?? '')).toUpperCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
protected onImageError(): void {
|
||||||
|
this.imageFailed.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
libs/ui/src/lib/ui/avatar/index.ts
Normal file
1
libs/ui/src/lib/ui/avatar/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './avatar/avatar.component';
|
||||||
9
libs/ui/src/lib/ui/badge/badge/badge.component.html
Normal file
9
libs/ui/src/lib/ui/badge/badge/badge.component.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<ng-content />
|
||||||
|
|
||||||
|
@if (!isHidden()) {
|
||||||
|
<span [class]="indicatorClasses">
|
||||||
|
@if (hasValue()) {
|
||||||
|
{{ displayValue() }}
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
97
libs/ui/src/lib/ui/badge/badge/badge.component.scss
Normal file
97
libs/ui/src/lib/ui/badge/badge/badge.component.scss
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
@mixin badgeColor($background, $color) {
|
||||||
|
background-color: $background;
|
||||||
|
color: $color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin badgePosition($vertical, $horizontal, $translate-x, $translate-y) {
|
||||||
|
#{$vertical}: 0;
|
||||||
|
#{$horizontal}: 0;
|
||||||
|
transform: translate($translate-x, $translate-y);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pi-badge__indicator {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
line-height: 1;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
// Position
|
||||||
|
&.pi-badge__position--top-right {
|
||||||
|
@include badgePosition(top, right, 50%, -50%);
|
||||||
|
}
|
||||||
|
&.pi-badge__position--top-left {
|
||||||
|
@include badgePosition(top, left, -50%, -50%);
|
||||||
|
}
|
||||||
|
&.pi-badge__position--bottom-right {
|
||||||
|
@include badgePosition(bottom, right, 50%, 50%);
|
||||||
|
}
|
||||||
|
&.pi-badge__position--bottom-left {
|
||||||
|
@include badgePosition(bottom, left, -50%, 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size (with value)
|
||||||
|
&.pi-badge__size--medium {
|
||||||
|
min-width: 1.125rem;
|
||||||
|
height: 1.125rem;
|
||||||
|
padding: 0 var(--pi-spacing-xxx-small);
|
||||||
|
font-size: var(--pi-font-size-xxx-small);
|
||||||
|
}
|
||||||
|
&.pi-badge__size--small {
|
||||||
|
min-width: 0.875rem;
|
||||||
|
height: 0.875rem;
|
||||||
|
padding: 0 var(--pi-spacing-xxxx-small);
|
||||||
|
font-size: 0.4375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dot (no value)
|
||||||
|
&.pi-badge__indicator--dot {
|
||||||
|
padding: 0;
|
||||||
|
&.pi-badge__size--medium {
|
||||||
|
min-width: 0.5rem;
|
||||||
|
width: 0.5rem;
|
||||||
|
height: 0.5rem;
|
||||||
|
}
|
||||||
|
&.pi-badge__size--small {
|
||||||
|
min-width: 0.375rem;
|
||||||
|
width: 0.375rem;
|
||||||
|
height: 0.375rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color
|
||||||
|
&.pi-color__primary {
|
||||||
|
@include badgeColor(var(--pi-color-primary), var(--pi-color-white));
|
||||||
|
}
|
||||||
|
&.pi-color__accent {
|
||||||
|
@include badgeColor(var(--pi-color-accent), var(--pi-color-black));
|
||||||
|
}
|
||||||
|
&.pi-color__warn {
|
||||||
|
@include badgeColor(var(--pi-color-warn), var(--pi-color-white));
|
||||||
|
}
|
||||||
|
&.pi-color__success {
|
||||||
|
@include badgeColor(var(--pi-color-success), var(--pi-color-white));
|
||||||
|
}
|
||||||
|
&.pi-color__highlighted {
|
||||||
|
@include badgeColor(var(--pi-color-highlighted), var(--pi-color-white));
|
||||||
|
}
|
||||||
|
&.pi-color__grey {
|
||||||
|
@include badgeColor(var(--pi-color-grey), var(--pi-color-white));
|
||||||
|
}
|
||||||
|
&.pi-color__black {
|
||||||
|
@include badgeColor(var(--pi-color-black), var(--pi-color-white));
|
||||||
|
}
|
||||||
|
&.pi-color__white {
|
||||||
|
@include badgeColor(var(--pi-color-white), var(--pi-color-black));
|
||||||
|
border: 0.0625rem solid var(--pi-color-grey-200);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
libs/ui/src/lib/ui/badge/badge/badge.component.ts
Normal file
60
libs/ui/src/lib/ui/badge/badge/badge.component.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
HostBinding,
|
||||||
|
computed,
|
||||||
|
input,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
// Shared
|
||||||
|
import { styleType } from '../../../styles';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pi-badge',
|
||||||
|
templateUrl: './badge.component.html',
|
||||||
|
styleUrls: ['./badge.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
})
|
||||||
|
export class PiBadgeComponent {
|
||||||
|
@HostBinding('class') className = 'pi-badge';
|
||||||
|
|
||||||
|
// Content
|
||||||
|
/** Si no se define, se muestra un punto sin valor. */
|
||||||
|
readonly value = input<string | number>();
|
||||||
|
/** Solo aplica cuando `value` es numérico: por encima se muestra `${max}+`. */
|
||||||
|
readonly max = input(99);
|
||||||
|
|
||||||
|
// Skin
|
||||||
|
readonly color = input<styleType>('warn');
|
||||||
|
readonly position = input<
|
||||||
|
'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'
|
||||||
|
>('top-right');
|
||||||
|
readonly size = input<'small' | 'medium'>('medium');
|
||||||
|
|
||||||
|
/** Oculta el indicador sin desmontar el contenido proyectado. */
|
||||||
|
readonly isHidden = input(false);
|
||||||
|
|
||||||
|
protected readonly hasValue = computed(() => {
|
||||||
|
const value = this.value();
|
||||||
|
return value !== undefined && value !== null && value !== '';
|
||||||
|
});
|
||||||
|
|
||||||
|
protected readonly displayValue = computed(() => {
|
||||||
|
const value = this.value();
|
||||||
|
if (typeof value === 'number') {
|
||||||
|
const max = this.max();
|
||||||
|
return value > max ? `${max}+` : `${value}`;
|
||||||
|
}
|
||||||
|
return value ?? '';
|
||||||
|
});
|
||||||
|
|
||||||
|
protected get indicatorClasses(): string {
|
||||||
|
return [
|
||||||
|
'pi-badge__indicator',
|
||||||
|
`pi-badge__position--${this.position()}`,
|
||||||
|
`pi-badge__size--${this.size()}`,
|
||||||
|
`pi-color__${this.color()}`,
|
||||||
|
this.hasValue() ? '' : 'pi-badge__indicator--dot',
|
||||||
|
].join(' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
1
libs/ui/src/lib/ui/badge/index.ts
Normal file
1
libs/ui/src/lib/ui/badge/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './badge/badge.component';
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
@if (text()) {
|
@if (text()) {
|
||||||
<span>{{ text() }}</span>
|
<span>{{ text() }}</span>
|
||||||
}
|
}
|
||||||
<span #projectedText><ng-content></ng-content></span>
|
<span #projectedText class="pi-icon-button__projected"><ng-content></ng-content></span>
|
||||||
<img src="/assets/icons/{{ icon() }}.svg" />
|
<img src="/assets/icons/{{ icon() }}.svg" />
|
||||||
</button>
|
</button>
|
||||||
} @else {
|
} @else {
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
@if (text()) {
|
@if (text()) {
|
||||||
<span>{{ text() }}</span>
|
<span>{{ text() }}</span>
|
||||||
}
|
}
|
||||||
<span #projectedText><ng-content></ng-content></span>
|
<span #projectedText class="pi-icon-button__projected"><ng-content></ng-content></span>
|
||||||
@if (!previous()) {
|
@if (!previous()) {
|
||||||
<mat-icon [svgIcon]="icon()!" alt="icon"></mat-icon>
|
<mat-icon [svgIcon]="icon()!" alt="icon"></mat-icon>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,14 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: var(--pi-spacing-xx-small);
|
gap: var(--pi-spacing-xx-small);
|
||||||
|
|
||||||
|
// `#projectedText` se pinta siempre (se usa para detectar si hay
|
||||||
|
// contenido proyectado), pero sin `display: contents` cuenta como hijo
|
||||||
|
// flex vacío y el `gap` de arriba desplaza el icono del centro real
|
||||||
|
// cuando no se proyecta nada (p. ej. `<pi-icon-button icon="x" />`).
|
||||||
|
.pi-icon-button__projected {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
&:not(:disabled) {
|
&:not(:disabled) {
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -6,15 +6,11 @@ import {
|
|||||||
effect,
|
effect,
|
||||||
inject,
|
inject,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
|
|
||||||
import { DataTableItem } from '../entities/data-table-item.model';
|
import { DataTableItem } from '../entities/data-table-item.model';
|
||||||
|
|
||||||
import { ComponentFamilyClass } from '../../../styles';
|
import { ComponentFamilyClass } from '../../../styles';
|
||||||
import {
|
import { DeviceConfigsFacade } from '../../../util/device-configs';
|
||||||
DeviceConfigsState,
|
|
||||||
DeviceTypeSelectors,
|
|
||||||
} from '../../../util/device-configs';
|
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -37,7 +33,7 @@ export class PiBaseDataTableComponent {
|
|||||||
readonly type = input<'header' | 'data'>('data');
|
readonly type = input<'header' | 'data'>('data');
|
||||||
readonly hideValues = input(false);
|
readonly hideValues = input(false);
|
||||||
|
|
||||||
private readonly store = inject<Store<DeviceConfigsState>>(Store);
|
private readonly deviceConfigsFacade = inject(DeviceConfigsFacade);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// `hideHeader` lee inputs; las otras dos líneas del antiguo `ngOnInit` solo
|
// `hideHeader` lee inputs; las otras dos líneas del antiguo `ngOnInit` solo
|
||||||
@@ -54,10 +50,9 @@ export class PiBaseDataTableComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private hideHeader(): void {
|
private hideHeader(): void {
|
||||||
this.store
|
this.deviceConfigsFacade.device$
|
||||||
.select(DeviceTypeSelectors.getDevice)
|
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe((deviceType: string) => {
|
.subscribe((deviceType) => {
|
||||||
if (deviceType !== 'DESKTOP' && this.hideValues()) {
|
if (deviceType !== 'DESKTOP' && this.hideValues()) {
|
||||||
delete this.items()?.[0]?.values;
|
delete this.items()?.[0]?.values;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,8 @@ import {
|
|||||||
input,
|
input,
|
||||||
contentChild,
|
contentChild,
|
||||||
viewChild,
|
viewChild,
|
||||||
inject,
|
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { textStyle, justify, borderType } from '../../../styles';
|
import { textStyle, justify, borderType } from '../../../styles';
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
|
|
||||||
// Store
|
|
||||||
import { DeviceConfigsState } from '../../../util/device-configs';
|
|
||||||
|
|
||||||
// RxJS
|
|
||||||
import { take } from 'rxjs/operators';
|
|
||||||
import { NgTemplateOutlet } from '@angular/common';
|
import { NgTemplateOutlet } from '@angular/common';
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'pi-data-table-item',
|
selector: 'pi-data-table-item',
|
||||||
@@ -42,8 +34,6 @@ export class PiDataTableItemComponent implements AfterViewInit {
|
|||||||
readonly item = contentChild<TemplateRef<any>>('item');
|
readonly item = contentChild<TemplateRef<any>>('item');
|
||||||
readonly componentItem = viewChild<ElementRef>('componentItem');
|
readonly componentItem = viewChild<ElementRef>('componentItem');
|
||||||
|
|
||||||
private readonly store = inject<Store<DeviceConfigsState>>(Store);
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
this.setCellClassByIndex();
|
this.setCellClassByIndex();
|
||||||
}
|
}
|
||||||
|
|||||||
10
libs/ui/src/lib/ui/drawer/drawer/drawer.component.html
Normal file
10
libs/ui/src/lib/ui/drawer/drawer/drawer.component.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<header>
|
||||||
|
@if (data.title) {
|
||||||
|
<span bold l>{{ data.title }}</span>
|
||||||
|
}
|
||||||
|
<pi-close-button size="small" [hasLabel]="false" (click)="close()" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<ng-container [ngTemplateOutlet]="data.template" />
|
||||||
|
</main>
|
||||||
21
libs/ui/src/lib/ui/drawer/drawer/drawer.component.scss
Normal file
21
libs/ui/src/lib/ui/drawer/drawer/drawer.component.scss
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--pi-spacing-x-large);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
libs/ui/src/lib/ui/drawer/drawer/drawer.component.ts
Normal file
31
libs/ui/src/lib/ui/drawer/drawer/drawer.component.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, HostBinding, inject } from '@angular/core';
|
||||||
|
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||||
|
import { NgTemplateOutlet } from '@angular/common';
|
||||||
|
|
||||||
|
import { DrawerData } from '../entities/drawer.model';
|
||||||
|
import { PiCloseButtonComponent } from '../../button/close-button/close-button.component';
|
||||||
|
import { PiTextWeightBoldDirective } from '../../text/directives/text-weights.directive';
|
||||||
|
import { PiTextLargeDirective } from '../../text/directives/text-sizes.directive';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'pi-drawer',
|
||||||
|
templateUrl: './drawer.component.html',
|
||||||
|
styleUrls: ['./drawer.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
imports: [
|
||||||
|
NgTemplateOutlet,
|
||||||
|
PiCloseButtonComponent,
|
||||||
|
PiTextWeightBoldDirective,
|
||||||
|
PiTextLargeDirective,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class PiDrawerComponent {
|
||||||
|
@HostBinding('class') className = 'pi-drawer';
|
||||||
|
|
||||||
|
readonly dialogRef = inject<MatDialogRef<PiDrawerComponent>>(MatDialogRef);
|
||||||
|
readonly data = inject<DrawerData>(MAT_DIALOG_DATA);
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.dialogRef.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
41
libs/ui/src/lib/ui/drawer/entities/drawer-configs.ts
Normal file
41
libs/ui/src/lib/ui/drawer/entities/drawer-configs.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { MatDialogConfig } from '@angular/material/dialog';
|
||||||
|
|
||||||
|
export enum DrawerConfigKeys {
|
||||||
|
Left = 'left',
|
||||||
|
Right = 'right',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DrawerConfigs = {
|
||||||
|
[key in DrawerConfigKeys]: MatDialogConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `enterAnimationDuration`/`exitAnimationDuration` a `0ms`: la animación de
|
||||||
|
* `MatDialog` (fade + scale desde el centro) no encaja con un panel anclado
|
||||||
|
* al borde. El deslizamiento real lo hace `_mat-drawer.scss` sobre
|
||||||
|
* `panelClass`, fuera del ciclo de animación de Material.
|
||||||
|
*/
|
||||||
|
export const drawerConfigs: DrawerConfigs = {
|
||||||
|
[DrawerConfigKeys.Right]: {
|
||||||
|
height: '100vh',
|
||||||
|
maxHeight: '100vh',
|
||||||
|
width: '24rem',
|
||||||
|
maxWidth: '90vw',
|
||||||
|
position: { top: '0', right: '0' },
|
||||||
|
backdropClass: 'pi-backdrop',
|
||||||
|
panelClass: ['pi-drawer__panel', 'pi-drawer__panel--right'],
|
||||||
|
enterAnimationDuration: '0ms',
|
||||||
|
exitAnimationDuration: '0ms',
|
||||||
|
},
|
||||||
|
[DrawerConfigKeys.Left]: {
|
||||||
|
height: '100vh',
|
||||||
|
maxHeight: '100vh',
|
||||||
|
width: '24rem',
|
||||||
|
maxWidth: '90vw',
|
||||||
|
position: { top: '0', left: '0' },
|
||||||
|
backdropClass: 'pi-backdrop',
|
||||||
|
panelClass: ['pi-drawer__panel', 'pi-drawer__panel--left'],
|
||||||
|
enterAnimationDuration: '0ms',
|
||||||
|
exitAnimationDuration: '0ms',
|
||||||
|
},
|
||||||
|
};
|
||||||
6
libs/ui/src/lib/ui/drawer/entities/drawer.model.ts
Normal file
6
libs/ui/src/lib/ui/drawer/entities/drawer.model.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { TemplateRef } from '@angular/core';
|
||||||
|
|
||||||
|
export interface DrawerData {
|
||||||
|
title?: string;
|
||||||
|
template: TemplateRef<any>;
|
||||||
|
}
|
||||||
9
libs/ui/src/lib/ui/drawer/index.ts
Normal file
9
libs/ui/src/lib/ui/drawer/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Component
|
||||||
|
export * from './drawer/drawer.component';
|
||||||
|
|
||||||
|
// Entities
|
||||||
|
export * from './entities/drawer.model';
|
||||||
|
export * from './entities/drawer-configs';
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
export * from './utils';
|
||||||
16
libs/ui/src/lib/ui/drawer/utils.ts
Normal file
16
libs/ui/src/lib/ui/drawer/utils.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { MatDialogConfig } from '@angular/material/dialog';
|
||||||
|
import { DrawerConfigKeys, drawerConfigs } from './entities/drawer-configs';
|
||||||
|
import { DrawerData } from './entities/drawer.model';
|
||||||
|
|
||||||
|
// Drawer config
|
||||||
|
export function getDrawerConfigs(
|
||||||
|
key: DrawerConfigKeys,
|
||||||
|
data: DrawerData,
|
||||||
|
disableClose?: boolean,
|
||||||
|
): MatDialogConfig<DrawerData> {
|
||||||
|
return {
|
||||||
|
...(disableClose && { disableClose }),
|
||||||
|
...drawerConfigs[key],
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,14 +5,8 @@ export * from './entities/notification.data';
|
|||||||
// Services
|
// Services
|
||||||
export * from './infrastructure/notification.service';
|
export * from './infrastructure/notification.service';
|
||||||
|
|
||||||
// State
|
|
||||||
export * from './state/notification.actions';
|
|
||||||
export * from './state/notification.effects';
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
export * from './utils';
|
export * from './utils';
|
||||||
|
|
||||||
// Module
|
|
||||||
|
|
||||||
// Components
|
// Components
|
||||||
export * from './snack-bar/snack-bar.component';
|
export * from './snack-bar/snack-bar.component';
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { createAction, props } from '@ngrx/store';
|
|
||||||
|
|
||||||
export const displaySuccess = createAction(
|
|
||||||
'[CORE NOTIFICATION] Display Success',
|
|
||||||
props<{
|
|
||||||
payload: string;
|
|
||||||
description?: string;
|
|
||||||
}>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const displayError = createAction(
|
|
||||||
'[CORE NOTIFICATION] Display Error',
|
|
||||||
props<{
|
|
||||||
payload: string;
|
|
||||||
description?: string;
|
|
||||||
}>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const displayInfo = createAction(
|
|
||||||
'[CORE NOTIFICATION] Display Info',
|
|
||||||
props<{
|
|
||||||
payload: string;
|
|
||||||
description?: string;
|
|
||||||
}>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const triggerNotificationAction = createAction(
|
|
||||||
'[CORE NOTIFICATION] Trigger Notification Action',
|
|
||||||
);
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
|
||||||
|
|
||||||
import * as NotificationActions from './notification.actions';
|
|
||||||
|
|
||||||
import { map, switchMap, tap } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
|
|
||||||
import { NotificationService } from '../infrastructure/notification.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class NotificationEffects {
|
|
||||||
constructor(
|
|
||||||
private action$: Actions,
|
|
||||||
private _notification: NotificationService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
displaySuccess$ = createEffect(
|
|
||||||
() => {
|
|
||||||
return this.action$.pipe(
|
|
||||||
ofType(NotificationActions.displaySuccess),
|
|
||||||
map((action) => {
|
|
||||||
return action;
|
|
||||||
}),
|
|
||||||
tap((action) => {
|
|
||||||
this._notification.success(action.payload, action.description);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ dispatch: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
displayError$ = createEffect(
|
|
||||||
() => {
|
|
||||||
return this.action$.pipe(
|
|
||||||
ofType(NotificationActions.displayError),
|
|
||||||
map((action) => {
|
|
||||||
return action;
|
|
||||||
}),
|
|
||||||
tap((action) => {
|
|
||||||
this._notification.error(action.payload, action.description);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ dispatch: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
displayInfo$ = createEffect(
|
|
||||||
() => {
|
|
||||||
return this.action$.pipe(
|
|
||||||
ofType(NotificationActions.displayInfo),
|
|
||||||
map((action) => {
|
|
||||||
return action;
|
|
||||||
}),
|
|
||||||
tap((action) => {
|
|
||||||
this._notification.info(action.payload, action.description);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ dispatch: false },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
1
libs/ui/src/lib/ui/popover/index.ts
Normal file
1
libs/ui/src/lib/ui/popover/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './popover.directive';
|
||||||
226
libs/ui/src/lib/ui/popover/popover.directive.ts
Normal file
226
libs/ui/src/lib/ui/popover/popover.directive.ts
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
import {
|
||||||
|
Directive,
|
||||||
|
ElementRef,
|
||||||
|
OnDestroy,
|
||||||
|
TemplateRef,
|
||||||
|
ViewContainerRef,
|
||||||
|
inject,
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
ConnectedPosition,
|
||||||
|
Overlay,
|
||||||
|
OverlayRef,
|
||||||
|
} from '@angular/cdk/overlay';
|
||||||
|
import { TemplatePortal } from '@angular/cdk/portal';
|
||||||
|
|
||||||
|
export type PopoverPosition = 'above' | 'below' | 'before' | 'after';
|
||||||
|
export type PopoverTrigger = 'click' | 'hover';
|
||||||
|
|
||||||
|
export interface PopoverContext {
|
||||||
|
close: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const GAP = 8;
|
||||||
|
|
||||||
|
/** Cada posición trae su opuesta como fallback si no cabe en el viewport. */
|
||||||
|
const POSITIONS: Record<PopoverPosition, ConnectedPosition[]> = {
|
||||||
|
below: [
|
||||||
|
{
|
||||||
|
originX: 'center',
|
||||||
|
originY: 'bottom',
|
||||||
|
overlayX: 'center',
|
||||||
|
overlayY: 'top',
|
||||||
|
offsetY: GAP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
originX: 'center',
|
||||||
|
originY: 'top',
|
||||||
|
overlayX: 'center',
|
||||||
|
overlayY: 'bottom',
|
||||||
|
offsetY: -GAP,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
above: [
|
||||||
|
{
|
||||||
|
originX: 'center',
|
||||||
|
originY: 'top',
|
||||||
|
overlayX: 'center',
|
||||||
|
overlayY: 'bottom',
|
||||||
|
offsetY: -GAP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
originX: 'center',
|
||||||
|
originY: 'bottom',
|
||||||
|
overlayX: 'center',
|
||||||
|
overlayY: 'top',
|
||||||
|
offsetY: GAP,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
after: [
|
||||||
|
{
|
||||||
|
originX: 'end',
|
||||||
|
originY: 'center',
|
||||||
|
overlayX: 'start',
|
||||||
|
overlayY: 'center',
|
||||||
|
offsetX: GAP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
originX: 'start',
|
||||||
|
originY: 'center',
|
||||||
|
overlayX: 'end',
|
||||||
|
overlayY: 'center',
|
||||||
|
offsetX: -GAP,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
before: [
|
||||||
|
{
|
||||||
|
originX: 'start',
|
||||||
|
originY: 'center',
|
||||||
|
overlayX: 'end',
|
||||||
|
overlayY: 'center',
|
||||||
|
offsetX: -GAP,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
originX: 'end',
|
||||||
|
originY: 'center',
|
||||||
|
overlayX: 'start',
|
||||||
|
overlayY: 'center',
|
||||||
|
offsetX: GAP,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A diferencia de `pi-tooltip` (envuelve `MatTooltip`, solo texto), aquí no
|
||||||
|
* hay componente Material equivalente para contenido enriquecido: se monta
|
||||||
|
* directamente sobre `@angular/cdk/overlay`.
|
||||||
|
*/
|
||||||
|
@Directive({
|
||||||
|
selector: '[pi-popover]',
|
||||||
|
host: {
|
||||||
|
'(click)': 'onHostClick()',
|
||||||
|
'(mouseenter)': 'onHostMouseEnter()',
|
||||||
|
'(mouseleave)': 'onHostMouseLeave()',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export class PiPopoverDirective implements OnDestroy {
|
||||||
|
readonly popoverContent = input<TemplateRef<PopoverContext>>();
|
||||||
|
readonly popoverPosition = input<PopoverPosition>('below');
|
||||||
|
readonly popoverTrigger = input<PopoverTrigger>('click');
|
||||||
|
readonly popoverPanelClass = input<string | string[]>('pi-popover__panel');
|
||||||
|
|
||||||
|
readonly opened = output<void>();
|
||||||
|
readonly closed = output<void>();
|
||||||
|
|
||||||
|
private readonly overlay = inject(Overlay);
|
||||||
|
private readonly elementRef = inject(ElementRef);
|
||||||
|
private readonly viewContainerRef = inject(ViewContainerRef);
|
||||||
|
|
||||||
|
private overlayRef?: OverlayRef;
|
||||||
|
private closeTimeoutId?: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
protected onHostClick(): void {
|
||||||
|
if (this.popoverTrigger() === 'click') this.toggle();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onHostMouseEnter(): void {
|
||||||
|
if (this.popoverTrigger() === 'hover') {
|
||||||
|
this.cancelScheduledClose();
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onHostMouseLeave(): void {
|
||||||
|
if (this.popoverTrigger() === 'hover') this.scheduleClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle(): void {
|
||||||
|
if (this.isOpen()) {
|
||||||
|
this.close();
|
||||||
|
} else {
|
||||||
|
this.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isOpen(): boolean {
|
||||||
|
return !!this.overlayRef?.hasAttached();
|
||||||
|
}
|
||||||
|
|
||||||
|
show(): void {
|
||||||
|
const template = this.popoverContent();
|
||||||
|
if (!template || this.isOpen()) return;
|
||||||
|
|
||||||
|
const overlayRef = this.getOverlayRef();
|
||||||
|
const context: PopoverContext = { close: () => this.close() };
|
||||||
|
overlayRef.attach(
|
||||||
|
new TemplatePortal(template, this.viewContainerRef, context),
|
||||||
|
);
|
||||||
|
this.opened.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
close(): void {
|
||||||
|
this.cancelScheduledClose();
|
||||||
|
if (!this.isOpen()) return;
|
||||||
|
this.overlayRef?.detach();
|
||||||
|
this.closed.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.overlayRef?.dispose();
|
||||||
|
this.cancelScheduledClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private getOverlayRef(): OverlayRef {
|
||||||
|
if (this.overlayRef) return this.overlayRef;
|
||||||
|
|
||||||
|
const isHoverTrigger = this.popoverTrigger() === 'hover';
|
||||||
|
|
||||||
|
const positionStrategy = this.overlay
|
||||||
|
.position()
|
||||||
|
.flexibleConnectedTo(this.elementRef)
|
||||||
|
.withPositions(POSITIONS[this.popoverPosition()])
|
||||||
|
.withFlexibleDimensions(false)
|
||||||
|
.withPush(true);
|
||||||
|
|
||||||
|
const overlayRef = this.overlay.create({
|
||||||
|
positionStrategy,
|
||||||
|
scrollStrategy: this.overlay.scrollStrategies.reposition(),
|
||||||
|
hasBackdrop: !isHoverTrigger,
|
||||||
|
backdropClass: 'cdk-overlay-transparent-backdrop',
|
||||||
|
panelClass: this.popoverPanelClass(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isHoverTrigger) {
|
||||||
|
overlayRef.backdropClick().subscribe(() => this.close());
|
||||||
|
} else {
|
||||||
|
// Sin backdrop en modo hover: el ratón entra/sale directamente del panel.
|
||||||
|
overlayRef.overlayElement.addEventListener('mouseenter', () =>
|
||||||
|
this.cancelScheduledClose(),
|
||||||
|
);
|
||||||
|
overlayRef.overlayElement.addEventListener('mouseleave', () =>
|
||||||
|
this.scheduleClose(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
overlayRef.keydownEvents().subscribe((event) => {
|
||||||
|
if (event.key === 'Escape') this.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.overlayRef = overlayRef;
|
||||||
|
return overlayRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleClose(): void {
|
||||||
|
this.cancelScheduledClose();
|
||||||
|
// Margen para que el ratón viaje del disparador al panel sin que cierre.
|
||||||
|
this.closeTimeoutId = setTimeout(() => this.close(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private cancelScheduledClose(): void {
|
||||||
|
if (this.closeTimeoutId) {
|
||||||
|
clearTimeout(this.closeTimeoutId);
|
||||||
|
this.closeTimeoutId = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +1,41 @@
|
|||||||
// Angular
|
// Angular
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
// Ngrx
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
|
|
||||||
// Rxjs
|
// Rxjs
|
||||||
import { Observable } from 'rxjs';
|
import { BehaviorSubject, Observable, combineLatest } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
import { Browser, Device, Size } from '../entities/device-configs.model';
|
import { Browser, Device, Size } from '../entities/device-configs.model';
|
||||||
import * as fromActions from '../state/device-configs.actions';
|
import { getUserBrowser } from '../services/device-configs.service';
|
||||||
import * as fromState from '../state/device-configs.reducer';
|
|
||||||
import * as fromSelectors from '../state/device-configs.selectors';
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class DeviceConfigsFacade {
|
export class DeviceConfigsFacade {
|
||||||
// `inject()` en vez de parámetro de constructor: los inicializadores de
|
private readonly deviceSubject = new BehaviorSubject<Device>('DESKTOP');
|
||||||
// propiedad de abajo se evalúan antes que los parámetros del constructor.
|
private readonly sizeSubject = new BehaviorSubject<Size | null>(null);
|
||||||
private readonly store = inject<Store<fromState.DeviceConfigsState>>(Store);
|
private readonly browserSubject = new BehaviorSubject<Browser>('UNKNOWN');
|
||||||
|
|
||||||
// Device
|
// Device
|
||||||
readonly device$: Observable<Device> = this.store.select(
|
readonly device$: Observable<Device> = this.deviceSubject.asObservable();
|
||||||
fromSelectors.getDevice,
|
|
||||||
);
|
|
||||||
// `null` hasta que se resuelve el tamaño (así lo declara el estado inicial)
|
// `null` hasta que se resuelve el tamaño (así lo declara el estado inicial)
|
||||||
readonly size$: Observable<Size | null> = this.store.select(
|
readonly size$: Observable<Size | null> = this.sizeSubject.asObservable();
|
||||||
fromSelectors.getSize,
|
readonly isSmallDevice$: Observable<boolean> = combineLatest([
|
||||||
);
|
this.device$,
|
||||||
readonly isSmallDevice$: Observable<boolean> = this.store.select(
|
this.size$,
|
||||||
fromSelectors.getIsSmallDevice,
|
]).pipe(map(([device, size]) => device !== 'DESKTOP' && size !== 'LARGE'));
|
||||||
);
|
|
||||||
|
|
||||||
// Browser
|
// Browser
|
||||||
readonly browser$: Observable<Browser> = this.store.select(
|
readonly browser$: Observable<Browser> = this.browserSubject.asObservable();
|
||||||
fromSelectors.getBrowser,
|
|
||||||
);
|
|
||||||
|
|
||||||
getBrowser(): void {
|
getBrowser(): void {
|
||||||
this.store.dispatch(fromActions.BrowserActions.getBrowser());
|
this.browserSubject.next(getUserBrowser());
|
||||||
}
|
}
|
||||||
|
|
||||||
setDevice(device: Device): void {
|
setDevice(device: Device): void {
|
||||||
this.store.dispatch(fromActions.DeviceActions.saveDeviceType({ device }));
|
this.deviceSubject.next(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSize(size: Size): void {
|
||||||
|
this.sizeSubject.next(size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,3 @@ export * from './entities/device-configs.model';
|
|||||||
|
|
||||||
// Services
|
// Services
|
||||||
export * from './services/device-configs.service';
|
export * from './services/device-configs.service';
|
||||||
|
|
||||||
// State
|
|
||||||
export * from './state/device-configs.actions';
|
|
||||||
export * from './state/device-configs.effects';
|
|
||||||
export * from './state/device-configs.selectors';
|
|
||||||
export * from './state/device-configs.reducer';
|
|
||||||
import * as DeviceTypeActions from './state/device-configs.actions';
|
|
||||||
import * as DeviceTypeSelectors from './state/device-configs.selectors';
|
|
||||||
export { DeviceTypeActions, DeviceTypeSelectors };
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { BreakpointObserver } from '@angular/cdk/layout';
|
import { BreakpointObserver } from '@angular/cdk/layout';
|
||||||
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
||||||
import { Inject, Injectable, PLATFORM_ID, inject } from '@angular/core';
|
import { Injectable, PLATFORM_ID, inject } from '@angular/core';
|
||||||
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
|
|
||||||
// Entities
|
// Entities
|
||||||
import {
|
import {
|
||||||
@@ -23,25 +21,9 @@ import {
|
|||||||
import { Browser } from '../entities/device-configs.model';
|
import { Browser } from '../entities/device-configs.model';
|
||||||
|
|
||||||
// State
|
// State
|
||||||
import * as fromActions from '../state/device-configs.actions';
|
import { DeviceConfigsFacade } from '../application/device-configs.facade';
|
||||||
import { DeviceConfigsState } from '../state/device-configs.reducer';
|
|
||||||
|
|
||||||
@Injectable({
|
export function getUserBrowser(): Browser {
|
||||||
providedIn: 'root',
|
|
||||||
})
|
|
||||||
export class DeviceConfigsService {
|
|
||||||
private htmlTag!: HTMLElement;
|
|
||||||
|
|
||||||
private readonly store = inject<Store<DeviceConfigsState>>(Store);
|
|
||||||
private readonly breakpointObserver = inject(BreakpointObserver);
|
|
||||||
private readonly document = inject<Document>(DOCUMENT);
|
|
||||||
readonly platformId = inject<Object>(PLATFORM_ID);
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.deviceConfigsHandler();
|
|
||||||
}
|
|
||||||
|
|
||||||
getUserBrowser(): Browser {
|
|
||||||
const userAgent = window.navigator.userAgent;
|
const userAgent = window.navigator.userAgent;
|
||||||
if (/MSIE|Trident/.test(userAgent)) {
|
if (/MSIE|Trident/.test(userAgent)) {
|
||||||
return 'EXPLORER';
|
return 'EXPLORER';
|
||||||
@@ -58,6 +40,25 @@ export class DeviceConfigsService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class DeviceConfigsService {
|
||||||
|
private htmlTag!: HTMLElement;
|
||||||
|
|
||||||
|
private readonly deviceConfigsFacade = inject(DeviceConfigsFacade);
|
||||||
|
private readonly breakpointObserver = inject(BreakpointObserver);
|
||||||
|
private readonly document = inject<Document>(DOCUMENT);
|
||||||
|
readonly platformId = inject<Object>(PLATFORM_ID);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.deviceConfigsHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
getUserBrowser(): Browser {
|
||||||
|
return getUserBrowser();
|
||||||
|
}
|
||||||
|
|
||||||
private deviceConfigsHandler(): void {
|
private deviceConfigsHandler(): void {
|
||||||
if (isPlatformBrowser(this.platformId)) {
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
this.htmlTag = this.document?.documentElement;
|
this.htmlTag = this.document?.documentElement;
|
||||||
@@ -82,12 +83,8 @@ export class DeviceConfigsService {
|
|||||||
if (result.matches) {
|
if (result.matches) {
|
||||||
// Mobile
|
// Mobile
|
||||||
if (result.breakpoints[MaxMobileBreakpoint]) {
|
if (result.breakpoints[MaxMobileBreakpoint]) {
|
||||||
this.store.dispatch(
|
this.deviceConfigsFacade.setDevice('MOBILE');
|
||||||
fromActions.DeviceActions.saveDeviceType({ device: 'MOBILE' }),
|
this.deviceConfigsFacade.setSize('SMALL');
|
||||||
);
|
|
||||||
this.store.dispatch(
|
|
||||||
fromActions.DeviceActions.saveDeviceSize({ size: 'SMALL' }),
|
|
||||||
);
|
|
||||||
this.setClassIsMobile();
|
this.setClassIsMobile();
|
||||||
}
|
}
|
||||||
// Tablet
|
// Tablet
|
||||||
@@ -95,18 +92,14 @@ export class DeviceConfigsService {
|
|||||||
result.breakpoints[MinTabletBreakpoint] &&
|
result.breakpoints[MinTabletBreakpoint] &&
|
||||||
result.breakpoints[MaxTabletBreakpoint]
|
result.breakpoints[MaxTabletBreakpoint]
|
||||||
) {
|
) {
|
||||||
this.store.dispatch(
|
this.deviceConfigsFacade.setDevice('TABLET');
|
||||||
fromActions.DeviceActions.saveDeviceType({ device: 'TABLET' }),
|
|
||||||
);
|
|
||||||
this.setClassIsTablet();
|
this.setClassIsTablet();
|
||||||
// Tablet S
|
// Tablet S
|
||||||
if (
|
if (
|
||||||
result.breakpoints[MinTabletSmallBreakpoint] &&
|
result.breakpoints[MinTabletSmallBreakpoint] &&
|
||||||
result.breakpoints[MaxTabletSmallBreakpoint]
|
result.breakpoints[MaxTabletSmallBreakpoint]
|
||||||
) {
|
) {
|
||||||
this.store.dispatch(
|
this.deviceConfigsFacade.setSize('SMALL');
|
||||||
fromActions.DeviceActions.saveDeviceSize({ size: 'SMALL' }),
|
|
||||||
);
|
|
||||||
this.setClassIsTabletSmall();
|
this.setClassIsTabletSmall();
|
||||||
}
|
}
|
||||||
// Tablet L
|
// Tablet L
|
||||||
@@ -114,27 +107,21 @@ export class DeviceConfigsService {
|
|||||||
result.breakpoints[MinTabletLargeBreakpoint] &&
|
result.breakpoints[MinTabletLargeBreakpoint] &&
|
||||||
result.breakpoints[MaxTabletLargeBreakpoint]
|
result.breakpoints[MaxTabletLargeBreakpoint]
|
||||||
) {
|
) {
|
||||||
this.store.dispatch(
|
this.deviceConfigsFacade.setSize('LARGE');
|
||||||
fromActions.DeviceActions.saveDeviceSize({ size: 'LARGE' }),
|
|
||||||
);
|
|
||||||
this.setClassIsTabletLarge();
|
this.setClassIsTabletLarge();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Desktop
|
// Desktop
|
||||||
if (result.breakpoints[MinDesktopBreakpoint]) {
|
if (result.breakpoints[MinDesktopBreakpoint]) {
|
||||||
this.store.dispatch(
|
this.deviceConfigsFacade.setDevice('DESKTOP');
|
||||||
fromActions.DeviceActions.saveDeviceType({ device: 'DESKTOP' }),
|
|
||||||
);
|
|
||||||
this.setClassIsDesktop();
|
this.setClassIsDesktop();
|
||||||
// Desktop S
|
// Desktop S
|
||||||
if (
|
if (
|
||||||
result.breakpoints[MinDesktopSmallBreakpoint] &&
|
result.breakpoints[MinDesktopSmallBreakpoint] &&
|
||||||
result.breakpoints[MaxDesktopSmallBreakpoint]
|
result.breakpoints[MaxDesktopSmallBreakpoint]
|
||||||
) {
|
) {
|
||||||
this.store.dispatch(
|
this.deviceConfigsFacade.setSize('SMALL');
|
||||||
fromActions.DeviceActions.saveDeviceSize({ size: 'SMALL' }),
|
|
||||||
);
|
|
||||||
this.setClassIsDesktopSmall();
|
this.setClassIsDesktopSmall();
|
||||||
}
|
}
|
||||||
// Desktop M
|
// Desktop M
|
||||||
@@ -142,16 +129,12 @@ export class DeviceConfigsService {
|
|||||||
result.breakpoints[MinDesktopMediumBreakpoint] &&
|
result.breakpoints[MinDesktopMediumBreakpoint] &&
|
||||||
result.breakpoints[MaxDesktopMediumBreakpoint]
|
result.breakpoints[MaxDesktopMediumBreakpoint]
|
||||||
) {
|
) {
|
||||||
this.store.dispatch(
|
this.deviceConfigsFacade.setSize('MEDIUM');
|
||||||
fromActions.DeviceActions.saveDeviceSize({ size: 'MEDIUM' }),
|
|
||||||
);
|
|
||||||
this.setClassIsDesktopMedium();
|
this.setClassIsDesktopMedium();
|
||||||
}
|
}
|
||||||
// Desktop L
|
// Desktop L
|
||||||
if (result.breakpoints[MinDesktopLargeBreakpoint]) {
|
if (result.breakpoints[MinDesktopLargeBreakpoint]) {
|
||||||
this.store.dispatch(
|
this.deviceConfigsFacade.setSize('LARGE');
|
||||||
fromActions.DeviceActions.saveDeviceSize({ size: 'LARGE' }),
|
|
||||||
);
|
|
||||||
this.setClassIsDesktopLarge();
|
this.setClassIsDesktopLarge();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { createActionGroup, emptyProps, props } from '@ngrx/store';
|
|
||||||
|
|
||||||
// Entities
|
|
||||||
import { Device, Size, Browser } from '../entities/device-configs.model';
|
|
||||||
|
|
||||||
export const DeviceActions = createActionGroup({
|
|
||||||
source: '[Main] Get Device Breakpoints',
|
|
||||||
events: {
|
|
||||||
'Save Device Type': props<{ device: Device }>(),
|
|
||||||
'Save Device Size': props<{ size: Size }>(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const BrowserActions = createActionGroup({
|
|
||||||
source: '[Main] Get Browser',
|
|
||||||
events: {
|
|
||||||
'Get Browser': emptyProps(),
|
|
||||||
'Get Browser Success': props<{ response: Browser }>(),
|
|
||||||
'Get Browser Error': emptyProps(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
|
||||||
|
|
||||||
// Rxjs
|
|
||||||
import { of } from 'rxjs';
|
|
||||||
import { catchError, exhaustMap, map } from 'rxjs/operators';
|
|
||||||
|
|
||||||
// Ngrx
|
|
||||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
import * as fromActions from './device-configs.actions';
|
|
||||||
|
|
||||||
// Service
|
|
||||||
import { DeviceConfigsService } from '../services/device-configs.service';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class DeviceConfigsEffects {
|
|
||||||
private readonly deviceConfigsService = inject(DeviceConfigsService);
|
|
||||||
private readonly actions = inject(Actions);
|
|
||||||
|
|
||||||
getBrowser$ = createEffect(() => {
|
|
||||||
return this.actions.pipe(
|
|
||||||
ofType(fromActions.BrowserActions.getBrowser),
|
|
||||||
exhaustMap(() =>
|
|
||||||
of(this.deviceConfigsService.getUserBrowser()).pipe(
|
|
||||||
map((response) => {
|
|
||||||
return fromActions.BrowserActions.getBrowserSuccess({ response });
|
|
||||||
}),
|
|
||||||
catchError(() => {
|
|
||||||
return of(fromActions.BrowserActions.getBrowserError());
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { createReducer, on } from '@ngrx/store';
|
|
||||||
import * as fromActions from './device-configs.actions';
|
|
||||||
|
|
||||||
import { Device, Size, Browser } from '../entities/device-configs.model';
|
|
||||||
|
|
||||||
export const featureKey = 'CORE_deviceConfigs';
|
|
||||||
|
|
||||||
export interface DeviceConfigsState {
|
|
||||||
device: Device;
|
|
||||||
size: Size | null;
|
|
||||||
browser: Browser;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initialState: DeviceConfigsState = {
|
|
||||||
device: 'DESKTOP',
|
|
||||||
size: null,
|
|
||||||
browser: 'UNKNOWN',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deviceConfigsReducer = createReducer(
|
|
||||||
initialState,
|
|
||||||
on(fromActions.DeviceActions.saveDeviceType, (state, action) => {
|
|
||||||
return { ...state, device: action.device };
|
|
||||||
}),
|
|
||||||
|
|
||||||
on(fromActions.DeviceActions.saveDeviceSize, (state, action) => {
|
|
||||||
return { ...state, size: action.size };
|
|
||||||
}),
|
|
||||||
on(fromActions.BrowserActions.getBrowserSuccess, (state, action) => {
|
|
||||||
return { ...state, browser: action.response };
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { createSelector, createFeatureSelector } from '@ngrx/store';
|
|
||||||
|
|
||||||
import * as fromReducer from './device-configs.reducer';
|
|
||||||
|
|
||||||
export const getFeatureState =
|
|
||||||
createFeatureSelector<fromReducer.DeviceConfigsState>(fromReducer.featureKey);
|
|
||||||
|
|
||||||
// Device Breakpoints
|
|
||||||
export const getDevice = createSelector(
|
|
||||||
getFeatureState,
|
|
||||||
(state) => state.device,
|
|
||||||
);
|
|
||||||
export const getSize = createSelector(getFeatureState, (state) => state.size);
|
|
||||||
|
|
||||||
// Mobile & Tablet S
|
|
||||||
export const getIsSmallDevice = createSelector(getFeatureState, (state) => {
|
|
||||||
return state.device !== 'DESKTOP' && state.size !== 'LARGE';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Browser
|
|
||||||
export const getBrowser = createSelector(
|
|
||||||
getFeatureState,
|
|
||||||
(state) => state.browser,
|
|
||||||
);
|
|
||||||
@@ -1,32 +1,24 @@
|
|||||||
// Angular
|
// Angular
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
// Ngrx
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
|
|
||||||
// Rxjs
|
// Rxjs
|
||||||
import { Observable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
|
||||||
// State
|
export type ListMode = 'grid' | 'list';
|
||||||
import * as fromSelectors from '../state/list-configs.selectors';
|
|
||||||
import * as fromActions from '../state/list-configs.actions';
|
const initialMode: ListMode = 'list';
|
||||||
import * as fromState from '../state/list-configs.reducer';
|
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class ListConfigsFacade {
|
export class ListConfigsFacade {
|
||||||
// `inject()` en vez de parámetro de constructor: los inicializadores de
|
private readonly modeSubject = new BehaviorSubject<ListMode>(initialMode);
|
||||||
// propiedad de abajo se evalúan antes que los parámetros del constructor.
|
|
||||||
private readonly store = inject<Store<fromState.ListConfigsState>>(Store);
|
|
||||||
|
|
||||||
readonly mode$: Observable<fromState.ListMode> = this.store.select(
|
readonly mode$: Observable<ListMode> = this.modeSubject.asObservable();
|
||||||
fromSelectors.getListMode,
|
|
||||||
);
|
|
||||||
|
|
||||||
setListMode(payload: fromState.ListMode): void {
|
setListMode(payload: ListMode): void {
|
||||||
this.store.dispatch(fromActions.setListMode({ payload }));
|
this.modeSubject.next(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
resetListMode(): void {
|
resetListMode(): void {
|
||||||
this.store.dispatch(fromActions.resetListMode());
|
this.modeSubject.next(initialMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,2 @@
|
|||||||
// Facade
|
// Facade
|
||||||
export * from './application/list-configs.facade';
|
export * from './application/list-configs.facade';
|
||||||
|
|
||||||
// State
|
|
||||||
export * from './state/list-configs.actions';
|
|
||||||
export * from './state/list-configs.selectors';
|
|
||||||
export * from './state/list-configs.reducer';
|
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import { createAction, props } from '@ngrx/store';
|
|
||||||
|
|
||||||
import { ListMode } from './list-configs.reducer';
|
|
||||||
|
|
||||||
export enum ListModeActionsType {
|
|
||||||
SET_LIST_MODE = '[Main] Set List Mode',
|
|
||||||
RESET_LIST_MODE = '[Main] Reset List Mode',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const setListMode = createAction(
|
|
||||||
ListModeActionsType.SET_LIST_MODE,
|
|
||||||
props<{ payload: ListMode }>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const resetListMode = createAction(ListModeActionsType.RESET_LIST_MODE);
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { createReducer, on } from '@ngrx/store';
|
|
||||||
|
|
||||||
import { resetListMode, setListMode } from './list-configs.actions';
|
|
||||||
|
|
||||||
export const featureKey = 'CORE_listConfigs';
|
|
||||||
|
|
||||||
export interface ListConfigsState {
|
|
||||||
mode: ListMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ListMode = 'grid' | 'list';
|
|
||||||
|
|
||||||
export const initialState: ListConfigsState = {
|
|
||||||
mode: 'list',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const listConfigsReducer = createReducer(
|
|
||||||
initialState,
|
|
||||||
on(setListMode, (state, action) => {
|
|
||||||
return { ...state, mode: action.payload };
|
|
||||||
}),
|
|
||||||
on(resetListMode, (state) => {
|
|
||||||
return { ...initialState };
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
|
||||||
|
|
||||||
import * as fromReducer from './list-configs.reducer';
|
|
||||||
|
|
||||||
export const getFeatureState =
|
|
||||||
createFeatureSelector<fromReducer.ListConfigsState>(fromReducer.featureKey);
|
|
||||||
|
|
||||||
export const getListMode = createSelector(
|
|
||||||
getFeatureState,
|
|
||||||
(state) => state.mode,
|
|
||||||
);
|
|
||||||
@@ -1,32 +1,20 @@
|
|||||||
// Angular
|
// Angular
|
||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
// RxJS
|
// RxJS
|
||||||
import { Observable, Subscribable } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
|
|
||||||
// NgRx
|
@Injectable({ providedIn: 'root' })
|
||||||
import { Store, select } from '@ngrx/store';
|
|
||||||
|
|
||||||
// Store
|
|
||||||
import * as LoaderSelectors from '../state/loader.selectors';
|
|
||||||
import * as LoaderActions from '../state/loader.actions';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class LoaderFacade {
|
export class LoaderFacade {
|
||||||
// `inject()` en vez de parámetro de constructor: los inicializadores de
|
private readonly loaderSubject = new BehaviorSubject<boolean>(false);
|
||||||
// propiedad de abajo se evalúan antes que los parámetros del constructor.
|
|
||||||
private readonly store = inject(Store);
|
|
||||||
|
|
||||||
loader$: Observable<boolean> = this.store.pipe(
|
loader$: Observable<boolean> = this.loaderSubject.asObservable();
|
||||||
select(LoaderSelectors.getLoader),
|
|
||||||
);
|
|
||||||
showLoader$?: Observable<unknown> | Subscribable<unknown> | Promise<unknown>;
|
|
||||||
|
|
||||||
showLoader() {
|
showLoader(): void {
|
||||||
this.store.dispatch(LoaderActions.showLoader());
|
this.loaderSubject.next(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
hideLoader() {
|
hideLoader(): void {
|
||||||
this.store.dispatch(LoaderActions.hideLoader());
|
this.loaderSubject.next(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,2 @@
|
|||||||
// Module
|
|
||||||
export * from './util-loader.module';
|
|
||||||
|
|
||||||
// Facade
|
// Facade
|
||||||
export * from './aplication/loader.facade';
|
export * from './aplication/loader.facade';
|
||||||
|
|
||||||
// State
|
|
||||||
export * from './state/loader.actions';
|
|
||||||
export * from './state/loader.reducer';
|
|
||||||
export * from './state/loader.selectors';
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
// NgRx
|
|
||||||
import { createAction } from '@ngrx/store';
|
|
||||||
|
|
||||||
export const showLoader = createAction('[Loader] Show Loader');
|
|
||||||
|
|
||||||
export const hideLoader = createAction('[Loader] Hide Loader');
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// NgRx
|
|
||||||
import { createReducer, on, Action } from '@ngrx/store';
|
|
||||||
|
|
||||||
// Store
|
|
||||||
import * as LoaderActions from './loader.actions';
|
|
||||||
|
|
||||||
export const loadersKey = 'CORE_loaderState';
|
|
||||||
|
|
||||||
export interface LoaderState {
|
|
||||||
loader: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initialLoaderState: LoaderState = {
|
|
||||||
loader: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loaderReducer = createReducer(
|
|
||||||
initialLoaderState,
|
|
||||||
|
|
||||||
on(LoaderActions.showLoader, (state) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loader: true,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
on(LoaderActions.hideLoader, (state) => {
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
loader: false,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export function loaderReducerFunction(
|
|
||||||
state: LoaderState | undefined,
|
|
||||||
action: Action,
|
|
||||||
) {
|
|
||||||
return loaderReducer(state, action);
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
// NgRx
|
|
||||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
|
||||||
|
|
||||||
// Store
|
|
||||||
import { loadersKey, LoaderState } from './loader.reducer';
|
|
||||||
|
|
||||||
export const selectLoaderState = createFeatureSelector<LoaderState>(loadersKey);
|
|
||||||
|
|
||||||
export const getLoader = createSelector(selectLoaderState, (state) => {
|
|
||||||
return state.loader;
|
|
||||||
});
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { StoreModule } from '@ngrx/store';
|
|
||||||
|
|
||||||
import * as fromReducer from './state/loader.reducer';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
StoreModule.forFeature(
|
|
||||||
fromReducer.loadersKey,
|
|
||||||
fromReducer.loaderReducerFunction,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class PiSharedUtilLoadersModule {}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// Angular
|
|
||||||
import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
|
|
||||||
|
|
||||||
// Router
|
|
||||||
import { RouterStateUrl } from './state/router.reducer';
|
|
||||||
|
|
||||||
// Ngrx
|
|
||||||
import * as fromRouter from '@ngrx/router-store';
|
|
||||||
|
|
||||||
export class CustomSerializer implements fromRouter.RouterStateSerializer<RouterStateUrl> {
|
|
||||||
serialize(routerState: RouterStateSnapshot): RouterStateUrl {
|
|
||||||
const { url } = routerState;
|
|
||||||
const { queryParams } = routerState.root;
|
|
||||||
|
|
||||||
let route: ActivatedRouteSnapshot = routerState.root;
|
|
||||||
|
|
||||||
while (route.firstChild) {
|
|
||||||
route = route.firstChild;
|
|
||||||
}
|
|
||||||
const { params, routeConfig } = route;
|
|
||||||
|
|
||||||
// Only return an object including the URL, params and query params
|
|
||||||
// instead of the entire snapshot
|
|
||||||
return {
|
|
||||||
url,
|
|
||||||
params,
|
|
||||||
queryParams,
|
|
||||||
title: typeof routeConfig?.title === 'string' ? routeConfig.title : '',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,5 @@
|
|||||||
export * from './util-router-store.module';
|
// Facade
|
||||||
|
export * from './router.facade';
|
||||||
// State
|
|
||||||
export * from './state/router.actions';
|
|
||||||
export * from './state/router.effects';
|
|
||||||
export * from './state/router.reducer';
|
|
||||||
export * from './state/router.selectors';
|
|
||||||
|
|
||||||
// Error handler
|
// Error handler
|
||||||
export * from './error-handler';
|
export * from './error-handler';
|
||||||
|
|||||||
55
libs/ui/src/lib/util/router-store/router.facade.ts
Normal file
55
libs/ui/src/lib/util/router-store/router.facade.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// Angular
|
||||||
|
import { Injectable, inject } from '@angular/core';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { Title } from '@angular/platform-browser';
|
||||||
|
import { NavigationEnd, NavigationExtras, Router } from '@angular/router';
|
||||||
|
|
||||||
|
// RxJS
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { filter, map, startWith } from 'rxjs/operators';
|
||||||
|
|
||||||
|
export interface RouterNavigationPayload {
|
||||||
|
path: any[];
|
||||||
|
query?: object;
|
||||||
|
extras?: NavigationExtras;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class RouterFacade {
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
private readonly location = inject(Location);
|
||||||
|
private readonly title = inject(Title);
|
||||||
|
|
||||||
|
private readonly navigationEnd$: Observable<NavigationEnd> =
|
||||||
|
this.router.events.pipe(
|
||||||
|
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly route$: Observable<string> = this.navigationEnd$.pipe(
|
||||||
|
map(() => this.router.url),
|
||||||
|
startWith(this.router.url),
|
||||||
|
);
|
||||||
|
|
||||||
|
readonly title$: Observable<string> = this.navigationEnd$.pipe(
|
||||||
|
map(() => this.title.getTitle()),
|
||||||
|
startWith(this.title.getTitle()),
|
||||||
|
);
|
||||||
|
|
||||||
|
navigateTo({ path, query, extras }: RouterNavigationPayload): void {
|
||||||
|
this.router.navigate(path, { queryParams: query, ...extras });
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateRefreshTo({ path, query, extras }: RouterNavigationPayload): void {
|
||||||
|
this.router.routeReuseStrategy.shouldReuseRoute = () => false;
|
||||||
|
this.router.onSameUrlNavigation = 'reload';
|
||||||
|
this.router.navigate(path, { queryParams: query, ...extras });
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateBack(): void {
|
||||||
|
this.location.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
navigateForward(): void {
|
||||||
|
this.location.forward();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { NavigationExtras } from '@angular/router';
|
|
||||||
|
|
||||||
import { createAction, props } from '@ngrx/store';
|
|
||||||
|
|
||||||
export const navigateTo = createAction(
|
|
||||||
'[Core Router] Navigate To',
|
|
||||||
props<{
|
|
||||||
payload: {
|
|
||||||
path: any[];
|
|
||||||
query?: object;
|
|
||||||
extras?: NavigationExtras;
|
|
||||||
};
|
|
||||||
}>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const navigateRefreshTo = createAction(
|
|
||||||
'[Core Router] Navigate Refresh To',
|
|
||||||
props<{
|
|
||||||
payload: {
|
|
||||||
path: any[];
|
|
||||||
query?: object;
|
|
||||||
extras?: NavigationExtras;
|
|
||||||
};
|
|
||||||
}>(),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const navigateBack = createAction('[Core Router] Navigate Back');
|
|
||||||
|
|
||||||
export const navigateForward = createAction('[Core Router] Navigate Forward');
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
import { Actions, createEffect, ofType } from '@ngrx/effects';
|
|
||||||
|
|
||||||
import * as RouterActions from './router.actions';
|
|
||||||
|
|
||||||
import { map, tap } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Location } from '@angular/common';
|
|
||||||
import { Router } from '@angular/router';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class RouterEffects {
|
|
||||||
navigateTo$ = createEffect(
|
|
||||||
() => {
|
|
||||||
return this.action$.pipe(
|
|
||||||
ofType(RouterActions.navigateTo),
|
|
||||||
map((action) => {
|
|
||||||
return action.payload;
|
|
||||||
}),
|
|
||||||
tap(({ path, query: queryParams, extras }) => {
|
|
||||||
this._router.navigate(path, { queryParams, ...extras });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ dispatch: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
navigateRefreshTo$ = createEffect(
|
|
||||||
() => {
|
|
||||||
return this.action$.pipe(
|
|
||||||
ofType(RouterActions.navigateRefreshTo),
|
|
||||||
map((action) => {
|
|
||||||
return action.payload;
|
|
||||||
}),
|
|
||||||
tap(({ path, query: queryParams, extras }) => {
|
|
||||||
this._router.routeReuseStrategy.shouldReuseRoute = () => false;
|
|
||||||
this._router.onSameUrlNavigation = 'reload';
|
|
||||||
this._router.navigate(path, { queryParams, ...extras });
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ dispatch: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
navigateBack$ = createEffect(
|
|
||||||
() => {
|
|
||||||
return this.action$.pipe(
|
|
||||||
ofType(RouterActions.navigateBack),
|
|
||||||
tap(() => {
|
|
||||||
this._location.back();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ dispatch: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
navigateForward$ = createEffect(
|
|
||||||
() => {
|
|
||||||
return this.action$.pipe(
|
|
||||||
ofType(RouterActions.navigateForward),
|
|
||||||
tap(() => {
|
|
||||||
this._location.forward();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
{ dispatch: false },
|
|
||||||
);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private _location: Location,
|
|
||||||
private action$: Actions,
|
|
||||||
private _router: Router,
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import * as fromRouter from '@ngrx/router-store';
|
|
||||||
|
|
||||||
import { Params } from '@angular/router';
|
|
||||||
|
|
||||||
export const coreRouterFeatureKey = 'CORE_router';
|
|
||||||
|
|
||||||
export interface RouterStateUrl {
|
|
||||||
url: string;
|
|
||||||
queryParams: Params;
|
|
||||||
params: Params;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RouterState {
|
|
||||||
routerReducer: fromRouter.RouterReducerState<RouterStateUrl>;
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { createFeatureSelector, createSelector } from '@ngrx/store';
|
|
||||||
|
|
||||||
import * as fromRouter from '@ngrx/router-store';
|
|
||||||
|
|
||||||
import * as fromReducer from './router.reducer';
|
|
||||||
|
|
||||||
export const getRouterState = createFeatureSelector<
|
|
||||||
fromRouter.RouterReducerState<fromReducer.RouterStateUrl>
|
|
||||||
>(fromReducer.coreRouterFeatureKey);
|
|
||||||
|
|
||||||
export const getTitle = createSelector(getRouterState, (state) => {
|
|
||||||
return state?.state?.title;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getRoute = createSelector(
|
|
||||||
getRouterState,
|
|
||||||
(state) => state?.state?.url,
|
|
||||||
);
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { ErrorHandler, NgModule } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
import { RouterState, StoreRouterConnectingModule } from '@ngrx/router-store';
|
|
||||||
import { CustomSerializer } from './custom-route-serializer';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
StoreRouterConnectingModule.forRoot({
|
|
||||||
routerState: RouterState.Minimal,
|
|
||||||
serializer: CustomSerializer,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class PiSharedUtilRouterStoreModule {}
|
|
||||||
6
libs/ui/tsconfig.lib.publish.json
Normal file
6
libs/ui/tsconfig.lib.publish.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.lib.prod.json",
|
||||||
|
"angularCompilerOptions": {
|
||||||
|
"compilationMode": "partial"
|
||||||
|
}
|
||||||
|
}
|
||||||
311
package-lock.json
generated
311
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,9 @@
|
|||||||
"build": "nx build ui",
|
"build": "nx build ui",
|
||||||
"build:playground": "nx build playground",
|
"build:playground": "nx build playground",
|
||||||
"build:all": "nx run-many -t build",
|
"build:all": "nx run-many -t build",
|
||||||
|
"package:ui": "nx run ui:package",
|
||||||
|
"pack:ui": "nx run ui:package && npm pack ./dist/libs/ui --pack-destination ./dist",
|
||||||
|
"publish:ui": "nx run ui:package && npm publish ./dist/libs/ui",
|
||||||
"lint": "nx lint ui",
|
"lint": "nx lint ui",
|
||||||
"format": "prettier --write \"{apps,libs}/**/*.{ts,html,scss,md}\""
|
"format": "prettier --write \"{apps,libs}/**/*.{ts,html,scss,md}\""
|
||||||
},
|
},
|
||||||
@@ -27,9 +30,6 @@
|
|||||||
"@angular/router": "22.0.6",
|
"@angular/router": "22.0.6",
|
||||||
"@angular/ssr": "22.0.6",
|
"@angular/ssr": "22.0.6",
|
||||||
"@apollo/client": "^4.2.8",
|
"@apollo/client": "^4.2.8",
|
||||||
"@ngrx/effects": "^21.1.1",
|
|
||||||
"@ngrx/router-store": "^21.1.1",
|
|
||||||
"@ngrx/store": "^21.1.1",
|
|
||||||
"@ngx-translate/core": "^18.0.0",
|
"@ngx-translate/core": "^18.0.0",
|
||||||
"@ngx-translate/http-loader": "^18.0.0",
|
"@ngx-translate/http-loader": "^18.0.0",
|
||||||
"apollo-angular": "^14.1.0",
|
"apollo-angular": "^14.1.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user