prepara update
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -8,6 +8,9 @@ out-tsc
|
||||
# dependencies
|
||||
node_modules
|
||||
|
||||
# Credenciales de registro npm (Nexus u otro). Ver .npmrc.example.
|
||||
.npmrc
|
||||
|
||||
# IDEs and editors
|
||||
/.idea
|
||||
.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';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
import { provideRouter, withInMemoryScrolling } from '@angular/router';
|
||||
import { provideStore } from '@ngrx/store';
|
||||
|
||||
import { routes } from './app.routes';
|
||||
import {
|
||||
@@ -16,7 +15,7 @@ import {
|
||||
} from '@ngx-translate/core';
|
||||
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
|
||||
@@ -62,12 +61,15 @@ export const appConfig: ApplicationConfig = {
|
||||
// Los iconos son SVG registrados en el MatIconRegistry (assets/icons).
|
||||
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({
|
||||
fallbackLang: 'es',
|
||||
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 class="theme-toggles">
|
||||
<label class="theme-toggle">
|
||||
<span>Tema alternativo</span>
|
||||
<pi-slide-toggle [(checked)]="alternativeTheme" />
|
||||
</label>
|
||||
<div class="theme-switcher" role="radiogroup" aria-label="Tema">
|
||||
@for (option of themeOptions; track option.value) {
|
||||
<button
|
||||
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">
|
||||
<span>Modo oscuro</span>
|
||||
<pi-slide-toggle [(checked)]="darkMode" />
|
||||
@@ -32,16 +41,14 @@
|
||||
</nav>
|
||||
|
||||
<p class="theme-hint">
|
||||
Tema:
|
||||
<strong>{{ alternativeTheme() ? 'alternative' : 'default' }}</strong> ·
|
||||
<code
|
||||
>{{ alternativeTheme() ? 'pi-shell__alternative' : 'pi-shell__default'
|
||||
}}</code
|
||||
>
|
||||
en el <code><body></code>. Los dos temas comparten exactamente los
|
||||
Tema: <strong>{{ theme() }}</strong> ·
|
||||
<code>{{ shellClassByTheme[theme()] }}</code>
|
||||
en el <code><body></code>. Los cuatro temas comparten exactamente los
|
||||
mismos selectores: solo cambian los tokens <code>--pi-color-*</code> y
|
||||
<code>--pi-theme-*</code> que definen <code>themes/_default.scss</code> y
|
||||
<code>themes/_alternative.scss</code>.
|
||||
<code>--pi-theme-*</code> que definen <code>themes/_default.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>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -47,6 +47,30 @@
|
||||
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 {
|
||||
margin: 0.75rem 0 0;
|
||||
font-size: 0.8rem;
|
||||
|
||||
@@ -33,13 +33,30 @@ export class App {
|
||||
|
||||
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
|
||||
* que poner juntas (`pi-shell pi-shell__<tema>`); van en el `<body>` porque
|
||||
* los overlays de Material (diálogos, snackbars) se montan fuera de
|
||||
* `<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() {
|
||||
effect(() => {
|
||||
@@ -50,10 +67,9 @@ export class App {
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
const shell = this.alternativeTheme()
|
||||
? ShellClass.Alternative
|
||||
: ShellClass.Default;
|
||||
this.document.body.className = shell;
|
||||
this.document.body.className = this.shellClassByTheme[this.theme()];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type ThemeVariant = 'default' | 'alternative' | 'minimal' | 'gradient';
|
||||
|
||||
@@ -195,6 +195,26 @@
|
||||
<pi-divider color="primary" width="large" label="Separador" />
|
||||
</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
|
||||
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."
|
||||
@@ -481,6 +501,49 @@
|
||||
</button>
|
||||
</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">
|
||||
<div class="stack full">
|
||||
<pi-data-grid
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
TemplateRef,
|
||||
inject,
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
FormField,
|
||||
@@ -19,6 +21,8 @@ import {
|
||||
PiAccordionItemContentDirective,
|
||||
PiAccordionItemDirective,
|
||||
PiActionButtonComponent,
|
||||
PiAvatarComponent,
|
||||
PiBadgeComponent,
|
||||
PiBannerComponent,
|
||||
PiBaseDataTableComponent,
|
||||
PiCardBottomContentDirective,
|
||||
@@ -46,6 +50,9 @@ import {
|
||||
DatasetColor,
|
||||
Device,
|
||||
PiDividerComponent,
|
||||
DrawerConfigKeys,
|
||||
PiDrawerComponent,
|
||||
getDrawerConfigs,
|
||||
PiEmailFieldComponent,
|
||||
PiExpansionPanelComponent,
|
||||
PiGhostButtonDirective,
|
||||
@@ -83,6 +90,7 @@ import {
|
||||
PiPanelExpandableContentDirective,
|
||||
PiPasswordFieldComponent,
|
||||
PiPhoneFieldComponent,
|
||||
PiPopoverDirective,
|
||||
PiProgressBarComponent,
|
||||
PiRadioButtonComponent,
|
||||
PiRange,
|
||||
@@ -189,6 +197,8 @@ interface DemoModel {
|
||||
PiIconComponent,
|
||||
PiTextViewerComponent,
|
||||
PiDividerComponent,
|
||||
PiAvatarComponent,
|
||||
PiBadgeComponent,
|
||||
PiChipComponent,
|
||||
PiChipStatusComponent,
|
||||
PiChipValueComponent,
|
||||
@@ -205,6 +215,7 @@ interface DemoModel {
|
||||
PiListItemRightContentDirective,
|
||||
PiListItemRightIconDirective,
|
||||
PiTooltipDirective,
|
||||
PiPopoverDirective,
|
||||
PiNotFoundComponent,
|
||||
// Feedback
|
||||
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 ------------------------------ */
|
||||
|
||||
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:
|
||||
|
||||
| Símbolo | Se exporta desde | Se omite de |
|
||||
| ----------------------------------------------- | --------------------- | ------------------- |
|
||||
| ------------------------- | ---------------- | ------------------ |
|
||||
| `PiSlideToggleComponent` | `ui/form-field` | `ui/slide-toggle` |
|
||||
| `PiDisabledDirective` | `util/status` | `ui/text` |
|
||||
| `featureKey`, `getFeatureState`, `initialState` | `util/device-configs` | `util/list-configs` |
|
||||
|
||||
## 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.
|
||||
- **`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 —
|
||||
`prefer-inject` (51), `directive-selector` (46), `no-explicit-any` (35),
|
||||
`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": {}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": [
|
||||
|
||||
@@ -11,16 +11,7 @@ export * from './lib/util/device-configs';
|
||||
export * from './lib/util/environments';
|
||||
export * from './lib/util/icon-registry';
|
||||
export * from './lib/util/infinite-scroll';
|
||||
// ./lib/util/list-configs: omitido(s) por colisión con otro módulo -> featureKey, getFeatureState, initialState
|
||||
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/list-configs';
|
||||
export * from './lib/util/loaders';
|
||||
export * from './lib/util/local-storage';
|
||||
export * from './lib/util/menu';
|
||||
@@ -36,6 +27,8 @@ export * from './lib/util/value';
|
||||
export * from './lib/util/view-title';
|
||||
|
||||
export * from './lib/ui/accordion';
|
||||
export * from './lib/ui/avatar';
|
||||
export * from './lib/ui/badge';
|
||||
export * from './lib/ui/banner';
|
||||
export * from './lib/ui/button';
|
||||
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-table';
|
||||
export * from './lib/ui/divider';
|
||||
export * from './lib/ui/drawer';
|
||||
export * from './lib/ui/error-status-code';
|
||||
export * from './lib/ui/file-upload';
|
||||
export * from './lib/ui/form-field';
|
||||
@@ -57,6 +51,7 @@ export * from './lib/ui/note';
|
||||
export * from './lib/ui/notification';
|
||||
export * from './lib/ui/paginator';
|
||||
export * from './lib/ui/panel';
|
||||
export * from './lib/ui/popover';
|
||||
export * from './lib/ui/progress';
|
||||
export * from './lib/ui/select';
|
||||
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-note';
|
||||
@forward 'pi-panel';
|
||||
@forward 'pi-popover';
|
||||
@forward 'pi-spinner';
|
||||
@forward 'pi-tab';
|
||||
@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(.mat-form-field-invalid) {
|
||||
&:not(.mat-focused) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
@forward 'mat-chip';
|
||||
@forward 'mat-datepicker';
|
||||
@forward 'mat-dialog';
|
||||
@forward 'mat-drawer';
|
||||
@forward 'mat-expansion-panel';
|
||||
@forward 'mat-form-field';
|
||||
@forward 'mat-menu';
|
||||
|
||||
@@ -14,6 +14,10 @@ export const ShellClass = {
|
||||
Default: shellTheming('default'),
|
||||
/** Aspecto de marketing: degradados, brillo al hover y controles mayores. */
|
||||
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
|
||||
|
||||
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
|
||||
// `alternative` lo sobreescribe.
|
||||
// El orden importa: `default` declara el contrato completo de tokens y los
|
||||
// demás temas lo sobreescriben.
|
||||
@forward 'default';
|
||||
@forward 'alternative';
|
||||
@forward 'minimal';
|
||||
@forward 'gradient';
|
||||
|
||||
@use 'default' 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()) {
|
||||
<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" />
|
||||
</button>
|
||||
} @else {
|
||||
@@ -54,7 +54,7 @@
|
||||
@if (text()) {
|
||||
<span>{{ text() }}</span>
|
||||
}
|
||||
<span #projectedText><ng-content></ng-content></span>
|
||||
<span #projectedText class="pi-icon-button__projected"><ng-content></ng-content></span>
|
||||
@if (!previous()) {
|
||||
<mat-icon [svgIcon]="icon()!" alt="icon"></mat-icon>
|
||||
}
|
||||
|
||||
@@ -17,6 +17,14 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
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) {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
@@ -6,15 +6,11 @@ import {
|
||||
effect,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { Store } from '@ngrx/store';
|
||||
|
||||
import { DataTableItem } from '../entities/data-table-item.model';
|
||||
|
||||
import { ComponentFamilyClass } from '../../../styles';
|
||||
import {
|
||||
DeviceConfigsState,
|
||||
DeviceTypeSelectors,
|
||||
} from '../../../util/device-configs';
|
||||
import { DeviceConfigsFacade } from '../../../util/device-configs';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
@Component({
|
||||
@@ -37,7 +33,7 @@ export class PiBaseDataTableComponent {
|
||||
readonly type = input<'header' | 'data'>('data');
|
||||
readonly hideValues = input(false);
|
||||
|
||||
private readonly store = inject<Store<DeviceConfigsState>>(Store);
|
||||
private readonly deviceConfigsFacade = inject(DeviceConfigsFacade);
|
||||
|
||||
constructor() {
|
||||
// `hideHeader` lee inputs; las otras dos líneas del antiguo `ngOnInit` solo
|
||||
@@ -54,10 +50,9 @@ export class PiBaseDataTableComponent {
|
||||
}
|
||||
|
||||
private hideHeader(): void {
|
||||
this.store
|
||||
.select(DeviceTypeSelectors.getDevice)
|
||||
this.deviceConfigsFacade.device$
|
||||
.pipe(take(1))
|
||||
.subscribe((deviceType: string) => {
|
||||
.subscribe((deviceType) => {
|
||||
if (deviceType !== 'DESKTOP' && this.hideValues()) {
|
||||
delete this.items()?.[0]?.values;
|
||||
}
|
||||
|
||||
@@ -8,16 +8,8 @@ import {
|
||||
input,
|
||||
contentChild,
|
||||
viewChild,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
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';
|
||||
@Component({
|
||||
selector: 'pi-data-table-item',
|
||||
@@ -42,8 +34,6 @@ export class PiDataTableItemComponent implements AfterViewInit {
|
||||
readonly item = contentChild<TemplateRef<any>>('item');
|
||||
readonly componentItem = viewChild<ElementRef>('componentItem');
|
||||
|
||||
private readonly store = inject<Store<DeviceConfigsState>>(Store);
|
||||
|
||||
ngAfterViewInit(): void {
|
||||
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
|
||||
export * from './infrastructure/notification.service';
|
||||
|
||||
// State
|
||||
export * from './state/notification.actions';
|
||||
export * from './state/notification.effects';
|
||||
|
||||
// Utils
|
||||
export * from './utils';
|
||||
|
||||
// Module
|
||||
|
||||
// Components
|
||||
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
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
|
||||
// Ngrx
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
// Rxjs
|
||||
import { Observable } from 'rxjs';
|
||||
import { BehaviorSubject, Observable, combineLatest } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
// State
|
||||
import { Browser, Device, Size } from '../entities/device-configs.model';
|
||||
import * as fromActions from '../state/device-configs.actions';
|
||||
import * as fromState from '../state/device-configs.reducer';
|
||||
import * as fromSelectors from '../state/device-configs.selectors';
|
||||
import { getUserBrowser } from '../services/device-configs.service';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class DeviceConfigsFacade {
|
||||
// `inject()` en vez de parámetro de constructor: los inicializadores de
|
||||
// propiedad de abajo se evalúan antes que los parámetros del constructor.
|
||||
private readonly store = inject<Store<fromState.DeviceConfigsState>>(Store);
|
||||
private readonly deviceSubject = new BehaviorSubject<Device>('DESKTOP');
|
||||
private readonly sizeSubject = new BehaviorSubject<Size | null>(null);
|
||||
private readonly browserSubject = new BehaviorSubject<Browser>('UNKNOWN');
|
||||
|
||||
// Device
|
||||
readonly device$: Observable<Device> = this.store.select(
|
||||
fromSelectors.getDevice,
|
||||
);
|
||||
readonly device$: Observable<Device> = this.deviceSubject.asObservable();
|
||||
// `null` hasta que se resuelve el tamaño (así lo declara el estado inicial)
|
||||
readonly size$: Observable<Size | null> = this.store.select(
|
||||
fromSelectors.getSize,
|
||||
);
|
||||
readonly isSmallDevice$: Observable<boolean> = this.store.select(
|
||||
fromSelectors.getIsSmallDevice,
|
||||
);
|
||||
readonly size$: Observable<Size | null> = this.sizeSubject.asObservable();
|
||||
readonly isSmallDevice$: Observable<boolean> = combineLatest([
|
||||
this.device$,
|
||||
this.size$,
|
||||
]).pipe(map(([device, size]) => device !== 'DESKTOP' && size !== 'LARGE'));
|
||||
|
||||
// Browser
|
||||
readonly browser$: Observable<Browser> = this.store.select(
|
||||
fromSelectors.getBrowser,
|
||||
);
|
||||
readonly browser$: Observable<Browser> = this.browserSubject.asObservable();
|
||||
|
||||
getBrowser(): void {
|
||||
this.store.dispatch(fromActions.BrowserActions.getBrowser());
|
||||
this.browserSubject.next(getUserBrowser());
|
||||
}
|
||||
|
||||
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
|
||||
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 { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
||||
import { Inject, Injectable, PLATFORM_ID, inject } from '@angular/core';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Injectable, PLATFORM_ID, inject } from '@angular/core';
|
||||
|
||||
// Entities
|
||||
import {
|
||||
@@ -23,25 +21,9 @@ import {
|
||||
import { Browser } from '../entities/device-configs.model';
|
||||
|
||||
// State
|
||||
import * as fromActions from '../state/device-configs.actions';
|
||||
import { DeviceConfigsState } from '../state/device-configs.reducer';
|
||||
import { DeviceConfigsFacade } from '../application/device-configs.facade';
|
||||
|
||||
@Injectable({
|
||||
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 {
|
||||
export function getUserBrowser(): Browser {
|
||||
const userAgent = window.navigator.userAgent;
|
||||
if (/MSIE|Trident/.test(userAgent)) {
|
||||
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 {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
this.htmlTag = this.document?.documentElement;
|
||||
@@ -82,12 +83,8 @@ export class DeviceConfigsService {
|
||||
if (result.matches) {
|
||||
// Mobile
|
||||
if (result.breakpoints[MaxMobileBreakpoint]) {
|
||||
this.store.dispatch(
|
||||
fromActions.DeviceActions.saveDeviceType({ device: 'MOBILE' }),
|
||||
);
|
||||
this.store.dispatch(
|
||||
fromActions.DeviceActions.saveDeviceSize({ size: 'SMALL' }),
|
||||
);
|
||||
this.deviceConfigsFacade.setDevice('MOBILE');
|
||||
this.deviceConfigsFacade.setSize('SMALL');
|
||||
this.setClassIsMobile();
|
||||
}
|
||||
// Tablet
|
||||
@@ -95,18 +92,14 @@ export class DeviceConfigsService {
|
||||
result.breakpoints[MinTabletBreakpoint] &&
|
||||
result.breakpoints[MaxTabletBreakpoint]
|
||||
) {
|
||||
this.store.dispatch(
|
||||
fromActions.DeviceActions.saveDeviceType({ device: 'TABLET' }),
|
||||
);
|
||||
this.deviceConfigsFacade.setDevice('TABLET');
|
||||
this.setClassIsTablet();
|
||||
// Tablet S
|
||||
if (
|
||||
result.breakpoints[MinTabletSmallBreakpoint] &&
|
||||
result.breakpoints[MaxTabletSmallBreakpoint]
|
||||
) {
|
||||
this.store.dispatch(
|
||||
fromActions.DeviceActions.saveDeviceSize({ size: 'SMALL' }),
|
||||
);
|
||||
this.deviceConfigsFacade.setSize('SMALL');
|
||||
this.setClassIsTabletSmall();
|
||||
}
|
||||
// Tablet L
|
||||
@@ -114,27 +107,21 @@ export class DeviceConfigsService {
|
||||
result.breakpoints[MinTabletLargeBreakpoint] &&
|
||||
result.breakpoints[MaxTabletLargeBreakpoint]
|
||||
) {
|
||||
this.store.dispatch(
|
||||
fromActions.DeviceActions.saveDeviceSize({ size: 'LARGE' }),
|
||||
);
|
||||
this.deviceConfigsFacade.setSize('LARGE');
|
||||
this.setClassIsTabletLarge();
|
||||
}
|
||||
}
|
||||
|
||||
// Desktop
|
||||
if (result.breakpoints[MinDesktopBreakpoint]) {
|
||||
this.store.dispatch(
|
||||
fromActions.DeviceActions.saveDeviceType({ device: 'DESKTOP' }),
|
||||
);
|
||||
this.deviceConfigsFacade.setDevice('DESKTOP');
|
||||
this.setClassIsDesktop();
|
||||
// Desktop S
|
||||
if (
|
||||
result.breakpoints[MinDesktopSmallBreakpoint] &&
|
||||
result.breakpoints[MaxDesktopSmallBreakpoint]
|
||||
) {
|
||||
this.store.dispatch(
|
||||
fromActions.DeviceActions.saveDeviceSize({ size: 'SMALL' }),
|
||||
);
|
||||
this.deviceConfigsFacade.setSize('SMALL');
|
||||
this.setClassIsDesktopSmall();
|
||||
}
|
||||
// Desktop M
|
||||
@@ -142,16 +129,12 @@ export class DeviceConfigsService {
|
||||
result.breakpoints[MinDesktopMediumBreakpoint] &&
|
||||
result.breakpoints[MaxDesktopMediumBreakpoint]
|
||||
) {
|
||||
this.store.dispatch(
|
||||
fromActions.DeviceActions.saveDeviceSize({ size: 'MEDIUM' }),
|
||||
);
|
||||
this.deviceConfigsFacade.setSize('MEDIUM');
|
||||
this.setClassIsDesktopMedium();
|
||||
}
|
||||
// Desktop L
|
||||
if (result.breakpoints[MinDesktopLargeBreakpoint]) {
|
||||
this.store.dispatch(
|
||||
fromActions.DeviceActions.saveDeviceSize({ size: 'LARGE' }),
|
||||
);
|
||||
this.deviceConfigsFacade.setSize('LARGE');
|
||||
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
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
|
||||
// Ngrx
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
// Rxjs
|
||||
import { Observable } from 'rxjs';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
// State
|
||||
import * as fromSelectors from '../state/list-configs.selectors';
|
||||
import * as fromActions from '../state/list-configs.actions';
|
||||
import * as fromState from '../state/list-configs.reducer';
|
||||
export type ListMode = 'grid' | 'list';
|
||||
|
||||
const initialMode: ListMode = 'list';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ListConfigsFacade {
|
||||
// `inject()` en vez de parámetro de constructor: los inicializadores de
|
||||
// propiedad de abajo se evalúan antes que los parámetros del constructor.
|
||||
private readonly store = inject<Store<fromState.ListConfigsState>>(Store);
|
||||
private readonly modeSubject = new BehaviorSubject<ListMode>(initialMode);
|
||||
|
||||
readonly mode$: Observable<fromState.ListMode> = this.store.select(
|
||||
fromSelectors.getListMode,
|
||||
);
|
||||
readonly mode$: Observable<ListMode> = this.modeSubject.asObservable();
|
||||
|
||||
setListMode(payload: fromState.ListMode): void {
|
||||
this.store.dispatch(fromActions.setListMode({ payload }));
|
||||
setListMode(payload: ListMode): void {
|
||||
this.modeSubject.next(payload);
|
||||
}
|
||||
|
||||
resetListMode(): void {
|
||||
this.store.dispatch(fromActions.resetListMode());
|
||||
this.modeSubject.next(initialMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,2 @@
|
||||
// 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
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
// RxJS
|
||||
import { Observable, Subscribable } from 'rxjs';
|
||||
import { BehaviorSubject, Observable } from 'rxjs';
|
||||
|
||||
// NgRx
|
||||
import { Store, select } from '@ngrx/store';
|
||||
|
||||
// Store
|
||||
import * as LoaderSelectors from '../state/loader.selectors';
|
||||
import * as LoaderActions from '../state/loader.actions';
|
||||
|
||||
@Injectable()
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class LoaderFacade {
|
||||
// `inject()` en vez de parámetro de constructor: los inicializadores de
|
||||
// propiedad de abajo se evalúan antes que los parámetros del constructor.
|
||||
private readonly store = inject(Store);
|
||||
private readonly loaderSubject = new BehaviorSubject<boolean>(false);
|
||||
|
||||
loader$: Observable<boolean> = this.store.pipe(
|
||||
select(LoaderSelectors.getLoader),
|
||||
);
|
||||
showLoader$?: Observable<unknown> | Subscribable<unknown> | Promise<unknown>;
|
||||
loader$: Observable<boolean> = this.loaderSubject.asObservable();
|
||||
|
||||
showLoader() {
|
||||
this.store.dispatch(LoaderActions.showLoader());
|
||||
showLoader(): void {
|
||||
this.loaderSubject.next(true);
|
||||
}
|
||||
|
||||
hideLoader() {
|
||||
this.store.dispatch(LoaderActions.hideLoader());
|
||||
hideLoader(): void {
|
||||
this.loaderSubject.next(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,2 @@
|
||||
// Module
|
||||
export * from './util-loader.module';
|
||||
|
||||
// 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';
|
||||
|
||||
// State
|
||||
export * from './state/router.actions';
|
||||
export * from './state/router.effects';
|
||||
export * from './state/router.reducer';
|
||||
export * from './state/router.selectors';
|
||||
// Facade
|
||||
export * from './router.facade';
|
||||
|
||||
// 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:playground": "nx build playground",
|
||||
"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",
|
||||
"format": "prettier --write \"{apps,libs}/**/*.{ts,html,scss,md}\""
|
||||
},
|
||||
@@ -27,9 +30,6 @@
|
||||
"@angular/router": "22.0.6",
|
||||
"@angular/ssr": "22.0.6",
|
||||
"@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/http-loader": "^18.0.0",
|
||||
"apollo-angular": "^14.1.0",
|
||||
|
||||
Reference in New Issue
Block a user