fix components
This commit is contained in:
@@ -5,7 +5,10 @@ import {
|
||||
provideBrowserGlobalErrorListeners,
|
||||
} 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 {
|
||||
TranslateLoader,
|
||||
TranslationObject,
|
||||
@@ -48,6 +51,12 @@ export const appConfig: ApplicationConfig = {
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideAnimationsAsync(),
|
||||
|
||||
provideRouter(
|
||||
routes,
|
||||
// Al cambiar de página se vuelve arriba; el catálogo es muy largo.
|
||||
withInMemoryScrolling({ scrollPositionRestoration: 'top' }),
|
||||
),
|
||||
|
||||
// Los iconos son SVG registrados en el MatIconRegistry (assets/icons).
|
||||
provideAppInitializer(() => inject(IconRegistryService).init()),
|
||||
|
||||
|
||||
48
apps/playground/src/app/app.html
Normal file
48
apps/playground/src/app/app.html
Normal file
@@ -0,0 +1,48 @@
|
||||
<header class="page-header">
|
||||
<div class="page-header__top">
|
||||
<div>
|
||||
<h1>@pi/ui</h1>
|
||||
<p>
|
||||
Catálogo de la librería migrada a Angular 22: componentes standalone,
|
||||
señales, OnPush y Signal Forms.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="theme-toggles">
|
||||
<label class="theme-toggle">
|
||||
<span>Tema alternativo</span>
|
||||
<pi-slide-toggle [(checked)]="alternativeTheme" />
|
||||
</label>
|
||||
<label class="theme-toggle">
|
||||
<span>Modo oscuro</span>
|
||||
<pi-slide-toggle [(checked)]="darkMode" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="page-nav">
|
||||
@for (link of navLinks; track link.path) {
|
||||
<a
|
||||
[routerLink]="link.path"
|
||||
routerLinkActive="is-active"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>{{ link.label }}</a
|
||||
>
|
||||
}
|
||||
</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
|
||||
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>.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<router-outlet />
|
||||
18
apps/playground/src/app/app.routes.ts
Normal file
18
apps/playground/src/app/app.routes.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Routes } from '@angular/router';
|
||||
|
||||
export const routes: Routes = [
|
||||
{ path: '', pathMatch: 'full', redirectTo: 'componentes' },
|
||||
{
|
||||
path: 'componentes',
|
||||
title: 'Componentes · @pi/ui',
|
||||
loadComponent: () =>
|
||||
import('./pages/catalog/catalog.page').then((m) => m.CatalogPage),
|
||||
},
|
||||
{
|
||||
path: 'layouts',
|
||||
title: 'Layouts · @pi/ui',
|
||||
loadComponent: () =>
|
||||
import('./pages/layouts/layouts.page').then((m) => m.LayoutsPage),
|
||||
},
|
||||
{ path: '**', redirectTo: 'componentes' },
|
||||
];
|
||||
85
apps/playground/src/app/app.scss
Normal file
85
apps/playground/src/app/app.scss
Normal file
@@ -0,0 +1,85 @@
|
||||
:host {
|
||||
display: block;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem 4rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
&__top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--pg-heading);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0 0;
|
||||
color: var(--pg-text-muted);
|
||||
max-width: 60ch;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-toggles {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.85rem;
|
||||
color: var(--pg-text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.theme-hint {
|
||||
margin: 0.75rem 0 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--pg-text-muted);
|
||||
max-width: 70ch;
|
||||
|
||||
code {
|
||||
padding: 0.05rem 0.3rem;
|
||||
border-radius: 4px;
|
||||
background: var(--pg-badge-bg);
|
||||
color: var(--pg-badge-text);
|
||||
font-size: 0.95em;
|
||||
}
|
||||
}
|
||||
|
||||
.page-nav {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 1rem;
|
||||
|
||||
a {
|
||||
padding: 0.35rem 0.9rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--pg-card-border);
|
||||
color: var(--pg-text-muted);
|
||||
font-size: 0.85rem;
|
||||
text-decoration: none;
|
||||
|
||||
&.is-active {
|
||||
border-color: transparent;
|
||||
background: var(--pg-badge-bg);
|
||||
color: var(--pg-badge-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
59
apps/playground/src/app/app.ts
Normal file
59
apps/playground/src/app/app.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
effect,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
||||
|
||||
import { PiSlideToggleComponent, ShellClass } from '@pi/ui';
|
||||
|
||||
/**
|
||||
* Shell del playground: cabecera, navegación y estado de tema. El contenido de
|
||||
* cada página vive en `pages/` y entra por el `<router-outlet>`.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.scss',
|
||||
imports: [RouterOutlet, RouterLink, RouterLinkActive, PiSlideToggleComponent],
|
||||
})
|
||||
export class App {
|
||||
protected readonly navLinks = [
|
||||
{ path: '/componentes', label: 'Componentes' },
|
||||
{ path: '/layouts', label: 'Layouts' },
|
||||
];
|
||||
|
||||
/* --- Tema y modo oscuro -------------------------------------------------- */
|
||||
|
||||
private readonly document = inject(DOCUMENT);
|
||||
|
||||
protected readonly darkMode = signal(false);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
this.document.documentElement.classList.toggle(
|
||||
'dark-mode',
|
||||
this.darkMode(),
|
||||
);
|
||||
});
|
||||
|
||||
effect(() => {
|
||||
const shell = this.alternativeTheme()
|
||||
? ShellClass.Alternative
|
||||
: ShellClass.Default;
|
||||
this.document.body.className = shell;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,4 @@
|
||||
<header class="page-header">
|
||||
<h1>@pi/ui</h1>
|
||||
<p>
|
||||
Catálogo de la librería migrada a Angular 22: componentes standalone,
|
||||
señales, OnPush y Signal Forms.
|
||||
</p>
|
||||
<span class="event">Último evento: {{ lastEvent() }}</span>
|
||||
</header>
|
||||
<span class="event">Último evento: {{ lastEvent() }}</span>
|
||||
|
||||
<main>
|
||||
<div class="col">
|
||||
@@ -48,6 +41,18 @@
|
||||
|
||||
<pi-search-field [formField]="f.search" placeholder="Buscar…" />
|
||||
|
||||
<pi-icon-field [formField]="f.website" icon="globe">
|
||||
<pi-label>Web</pi-label>
|
||||
</pi-icon-field>
|
||||
|
||||
<pi-phone-field [formField]="f.phone">
|
||||
<pi-label>Teléfono</pi-label>
|
||||
</pi-phone-field>
|
||||
|
||||
<pi-number-field [formField]="f.weight">
|
||||
<pi-label>Peso (kg)</pi-label>
|
||||
</pi-number-field>
|
||||
|
||||
<label class="inline">
|
||||
<span>Newsletter</span>
|
||||
<pi-slide-toggle [formField]="f.newsletter" />
|
||||
@@ -59,6 +64,45 @@
|
||||
label="Volumen"
|
||||
[maxValue]="100"
|
||||
/>
|
||||
|
||||
<div class="range-slider-caption">
|
||||
<span
|
||||
>Rango de precio (pi-range-slider no pinta su propia etiqueta)</span
|
||||
>
|
||||
<pi-range-slider
|
||||
[formField]="f.priceRange"
|
||||
unit="€"
|
||||
[minValue]="0"
|
||||
[maxValue]="100"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<pi-date-field [formField]="f.birthDate">
|
||||
<pi-label>Fecha de nacimiento</pi-label>
|
||||
</pi-date-field>
|
||||
|
||||
<pi-radio-group
|
||||
[formField]="f.shipping"
|
||||
[options]="shippingOptions"
|
||||
[columns]="1"
|
||||
/>
|
||||
|
||||
<pi-checkbox [formField]="f.terms" label="Acepto los términos" />
|
||||
|
||||
<pi-file-upload
|
||||
[formField]="f.attachments"
|
||||
[multiple]="true"
|
||||
accept="image/*,.pdf"
|
||||
[maxSizeBytes]="2097152"
|
||||
[maxFiles]="3"
|
||||
label="Arrastra ficheros o pulsa para elegir"
|
||||
hint="Imágenes o PDF · máx. 2 MB · hasta 3"
|
||||
(rejected)="log('rechazados: ' + $event.length)"
|
||||
/>
|
||||
|
||||
<button pi-primary w100 (click)="submitForm()">
|
||||
Enviar (muestra los errores)
|
||||
</button>
|
||||
</div>
|
||||
</app-section>
|
||||
</div>
|
||||
@@ -87,6 +131,24 @@
|
||||
<pi-chevron-button (toggle)="log('chevron')" />
|
||||
<pi-close-button (toggle)="log('close')" />
|
||||
<pi-hamburger (toggle)="log('hamburger')" />
|
||||
<pi-radio-button
|
||||
[isActive]="radioActive()"
|
||||
[isDisabled]="false"
|
||||
(execute)="radioActive.set(!radioActive())"
|
||||
/>
|
||||
<pi-switch-button
|
||||
[content]="['Mensual', 'Anual']"
|
||||
contentType="text"
|
||||
[(isActive)]="switchActive"
|
||||
(toggledTo)="log('switch ' + $event)"
|
||||
/>
|
||||
<pi-toggle-button
|
||||
text="Favorito"
|
||||
icon="star-fill"
|
||||
[isActive]="toggleActive()"
|
||||
[isDisabled]="false"
|
||||
(execute)="toggleActive.set(!toggleActive())"
|
||||
/>
|
||||
</app-section>
|
||||
|
||||
<app-section title="Iconos y texto">
|
||||
@@ -99,10 +161,58 @@
|
||||
appearance="fill"
|
||||
[isStroked]="true"
|
||||
/>
|
||||
<pi-icon
|
||||
icon="info"
|
||||
color="primary"
|
||||
size="large"
|
||||
pi-tooltip
|
||||
tooltipText="Esto usa PiTooltipDirective"
|
||||
tooltipPosition="above"
|
||||
/>
|
||||
<pi-text-viewer value="Texto con el visor" fontSize="m" color="black" />
|
||||
<pi-divider color="primary" width="large" label="Separador" />
|
||||
</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."
|
||||
>
|
||||
<div class="stack full">
|
||||
<pi-headline pathImg="404.svg" alt="Ilustración">
|
||||
<pi-headline-label-content>
|
||||
<h4>Bienvenido</h4>
|
||||
</pi-headline-label-content>
|
||||
<pi-headline-description-content>
|
||||
<p>Gestiona tu cuenta desde aquí.</p>
|
||||
</pi-headline-description-content>
|
||||
</pi-headline>
|
||||
|
||||
<pi-list-item borderType="bottom" color="white" [isClickable]="true">
|
||||
<pi-list-item-left-icon>
|
||||
<pi-icon icon="user" color="primary" />
|
||||
</pi-list-item-left-icon>
|
||||
<pi-list-item-left-content>
|
||||
<span>Perfil</span>
|
||||
</pi-list-item-left-content>
|
||||
<pi-list-item-right-icon>
|
||||
<pi-icon icon="chevron-forward" color="grey" />
|
||||
</pi-list-item-right-icon>
|
||||
</pi-list-item>
|
||||
|
||||
<pi-list-item borderType="bottom" color="white" [isClickable]="true">
|
||||
<pi-list-item-left-icon>
|
||||
<pi-icon icon="notification-on" color="primary" />
|
||||
</pi-list-item-left-icon>
|
||||
<pi-list-item-left-content>
|
||||
<span>Notificaciones</span>
|
||||
</pi-list-item-left-content>
|
||||
<pi-list-item-right-content>
|
||||
<pi-chip-status status="pending" label="3" />
|
||||
</pi-list-item-right-content>
|
||||
</pi-list-item>
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section title="Chips">
|
||||
<pi-chip label="Etiqueta" color="primary" />
|
||||
<pi-chip-status status="success" label="Correcto" />
|
||||
@@ -114,6 +224,359 @@
|
||||
[isActive]="true"
|
||||
(execute)="log('chip-action')"
|
||||
/>
|
||||
<pi-chip-time
|
||||
date="2026-07-28"
|
||||
time="2026-07-28T09:30:00Z"
|
||||
[isActive]="false"
|
||||
/>
|
||||
<pi-chip-date date="2026-07-28" [isActive]="false" />
|
||||
<pi-tile label="Ingresos" value="12.4K €" icon="coin" />
|
||||
</app-section>
|
||||
|
||||
<app-section title="Card">
|
||||
<pi-card
|
||||
alignment="flex-start"
|
||||
gap="m"
|
||||
innerPadding="l"
|
||||
context="public"
|
||||
[isClickable]="true"
|
||||
(execute)="log('card')"
|
||||
>
|
||||
<pi-card-top-content>
|
||||
<h4>Título de la tarjeta</h4>
|
||||
</pi-card-top-content>
|
||||
<pi-card-center-content>
|
||||
<p>Contenido de ejemplo para pi-card.</p>
|
||||
</pi-card-center-content>
|
||||
</pi-card>
|
||||
</app-section>
|
||||
|
||||
<app-section
|
||||
title="Panel"
|
||||
hint="pi-expansion-panel[type=form] usa Reactive Forms clásico, no Signal Forms."
|
||||
>
|
||||
<div class="stack full">
|
||||
<pi-panel
|
||||
title="Resumen"
|
||||
subtitle="Últimos 30 días"
|
||||
height="medium"
|
||||
outline="grey"
|
||||
status=""
|
||||
innerPadding="l"
|
||||
>
|
||||
<pi-panel-center-content>
|
||||
<p>Contenido central del panel.</p>
|
||||
</pi-panel-center-content>
|
||||
<pi-panel-bottom-content>
|
||||
<button pi-ghost (click)="log('panel bottom action')">
|
||||
Ver más
|
||||
</button>
|
||||
</pi-panel-bottom-content>
|
||||
</pi-panel>
|
||||
|
||||
<pi-expansion-panel
|
||||
type="form"
|
||||
title="Datos de contacto"
|
||||
height="small"
|
||||
outline="grey"
|
||||
status=""
|
||||
innerPadding="l"
|
||||
[(isCollapsed)]="isPanelCollapsed"
|
||||
(edit)="log('expansion-panel edit')"
|
||||
>
|
||||
<pi-panel-center-content>
|
||||
<p>{{ f.contactEmail().value() || 'Sin email' }}</p>
|
||||
</pi-panel-center-content>
|
||||
<pi-panel-expandable-content>
|
||||
<input
|
||||
[formField]="f.contactEmail"
|
||||
placeholder="email@ejemplo.com"
|
||||
/>
|
||||
</pi-panel-expandable-content>
|
||||
</pi-expansion-panel>
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section title="Accordion">
|
||||
<div class="stack full">
|
||||
<pi-accordion
|
||||
color="primary"
|
||||
appearance="outline"
|
||||
[(expandedItemIndex)]="expandedAccordionIndex"
|
||||
>
|
||||
<pi-accordion-item
|
||||
title="Facturación"
|
||||
subtitle="Datos de pago"
|
||||
status="info"
|
||||
statusLabel="Pendiente"
|
||||
>
|
||||
<ng-template pi-accordion-item-expandable-content>
|
||||
<p>Contenido expandible del ítem 1.</p>
|
||||
</ng-template>
|
||||
</pi-accordion-item>
|
||||
<pi-accordion-item
|
||||
title="Envío"
|
||||
status="success"
|
||||
statusLabel="Completo"
|
||||
>
|
||||
<ng-template pi-accordion-item-expandable-content>
|
||||
<p>Detalles de envío.</p>
|
||||
</ng-template>
|
||||
</pi-accordion-item>
|
||||
</pi-accordion>
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section title="Tab">
|
||||
<div class="stack full">
|
||||
<pi-tab
|
||||
[tabButtons]="tabs"
|
||||
[isSmallDevice]="false"
|
||||
(tabChange)="log('tab ' + $event)"
|
||||
>
|
||||
<ng-template pi-tab-content
|
||||
>Contenido de la pestaña General.</ng-template
|
||||
>
|
||||
<ng-template pi-tab-content
|
||||
>Contenido de la pestaña Facturación.</ng-template
|
||||
>
|
||||
<ng-template pi-tab-content
|
||||
>Contenido de la pestaña Avanzado.</ng-template
|
||||
>
|
||||
</pi-tab>
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section title="Banner">
|
||||
<div class="stack full">
|
||||
<pi-banner
|
||||
mode="dark"
|
||||
title="Nueva funcionalidad"
|
||||
message="Descubre el nuevo panel de control."
|
||||
buttonText="Ver más"
|
||||
pathImg="404.svg"
|
||||
extension="svg"
|
||||
(execute)="log('banner')"
|
||||
/>
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section title="Note">
|
||||
<div class="stack full">
|
||||
<pi-note
|
||||
title="Aviso"
|
||||
text="Tu suscripción caduca pronto."
|
||||
status="pending"
|
||||
gap="s"
|
||||
>
|
||||
<pi-note-action-content>
|
||||
<button pi-ghost (click)="log('note action')">Renovar</button>
|
||||
</pi-note-action-content>
|
||||
</pi-note>
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section
|
||||
title="Select"
|
||||
hint="pi-select* usa Signal Forms como el resto: [formField] enlaza el valor completo de la opción elegida. #optionTemplate/#selectedTemplate son content children obligatorios de la librería; sin ellos, Angular lanza NG0951."
|
||||
>
|
||||
<div class="stack full">
|
||||
<pi-select
|
||||
[formField]="f.country"
|
||||
keyIdProperty="id"
|
||||
label="País"
|
||||
placeholder="Selecciona un país"
|
||||
[options]="countries"
|
||||
>
|
||||
<ng-template #optionTemplate let-option>
|
||||
<pi-select-option [id]="option.id" [label]="option.name" />
|
||||
</ng-template>
|
||||
<ng-template #selectedTemplate></ng-template>
|
||||
</pi-select>
|
||||
|
||||
<pi-select-multiple
|
||||
[formField]="f.tags"
|
||||
keyIdProperty="id"
|
||||
label="Etiquetas"
|
||||
placeholder="Selecciona etiquetas"
|
||||
[options]="tags"
|
||||
[hasSelectAllOptions]="true"
|
||||
selectAllLabel="Todas"
|
||||
>
|
||||
<ng-template #optionTemplate let-option>
|
||||
<pi-select-option
|
||||
type="multiple"
|
||||
[id]="option.id"
|
||||
[label]="option.name"
|
||||
/>
|
||||
</ng-template>
|
||||
<ng-template #selectedTemplate></ng-template>
|
||||
</pi-select-multiple>
|
||||
|
||||
<pi-select-period
|
||||
[formField]="f.period"
|
||||
keyIdProperty="id"
|
||||
label="Periodo"
|
||||
placeholder="Selecciona un periodo"
|
||||
[options]="countries"
|
||||
>
|
||||
<ng-template #optionTemplate let-option>
|
||||
<pi-select-option [id]="option.id" [label]="option.name" />
|
||||
</ng-template>
|
||||
<ng-template #selectedTemplate></ng-template>
|
||||
</pi-select-period>
|
||||
|
||||
<pi-select-carousel
|
||||
[formField]="f.plan"
|
||||
keyIdProperty="id"
|
||||
[options]="plans"
|
||||
[itemsDesktop]="3"
|
||||
[device]="device"
|
||||
>
|
||||
<ng-template #optionTemplate let-option>
|
||||
<pi-select-option
|
||||
[id]="option.id"
|
||||
[label]="option.name"
|
||||
[description]="option.description"
|
||||
flexDirection="column"
|
||||
/>
|
||||
</ng-template>
|
||||
<ng-template #selectedTemplate></ng-template>
|
||||
</pi-select-carousel>
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section
|
||||
title="Modal y notificaciones"
|
||||
hint="pi-modal y pi-snack-bar no se usan como etiquetas: se abren desde TypeScript vía MatDialog y NotificationService."
|
||||
>
|
||||
<button pi-primary (click)="openDemoModal()">Abrir modal</button>
|
||||
<button pi-outline (click)="showSuccessNotification()">
|
||||
Notificación de éxito
|
||||
</button>
|
||||
<button pi-outline (click)="showErrorNotification()">
|
||||
Notificación de error
|
||||
</button>
|
||||
</app-section>
|
||||
|
||||
<app-section title="Data grid">
|
||||
<div class="stack full">
|
||||
<pi-data-grid
|
||||
[data]="employees"
|
||||
[columns]="3"
|
||||
[rows]="1"
|
||||
[index]="0"
|
||||
[isActive]="false"
|
||||
mode="list"
|
||||
dataDirection="column"
|
||||
(clickItem)="log('data-grid item')"
|
||||
>
|
||||
<ng-template #valuesTemplate let-value let-i="index">
|
||||
<span pi-cell [index]="i">{{ value.name }} — {{ value.role }}</span>
|
||||
</ng-template>
|
||||
</pi-data-grid>
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section title="Data table">
|
||||
<div class="stack full">
|
||||
<pi-base-data-table dataDirection="row" type="header">
|
||||
<pi-data-table-item
|
||||
heading="Concepto"
|
||||
[values]="['Cliente', 'Importe', 'Fecha']"
|
||||
>
|
||||
<ng-template #item let-value="value">
|
||||
<pi-cell-type type="text" [display]="value" />
|
||||
</ng-template>
|
||||
</pi-data-table-item>
|
||||
</pi-base-data-table>
|
||||
|
||||
@for (row of invoiceRows; track row) {
|
||||
<pi-base-data-table dataDirection="row" type="data">
|
||||
<pi-data-table-item [values]="row">
|
||||
<ng-template #item let-value="value" let-i="index">
|
||||
<pi-cell-type
|
||||
[type]="i === 1 ? 'amount' : 'text'"
|
||||
[display]="value"
|
||||
/>
|
||||
</ng-template>
|
||||
</pi-data-table-item>
|
||||
</pi-base-data-table>
|
||||
}
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section title="Chart">
|
||||
<div class="stack full">
|
||||
<pi-chart
|
||||
type="line"
|
||||
[device]="device"
|
||||
[xAxisMarks]="['Ene', 'Feb', 'Mar', 'Abr']"
|
||||
[datasets]="chartDatasets"
|
||||
/>
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section title="Carousel">
|
||||
<div class="stack full">
|
||||
<pi-carousel
|
||||
[itemsDesktop]="3"
|
||||
[itemsTablet]="2"
|
||||
[itemsMobile]="1"
|
||||
[hideSwipeButtons]="false"
|
||||
[device]="device"
|
||||
color="primary"
|
||||
(itemChanged)="log('carousel ' + $event)"
|
||||
>
|
||||
@for (item of carouselItems; track item.id) {
|
||||
<ng-template pi-carousel-item>
|
||||
<div class="demo-card">{{ item.label }}</div>
|
||||
</ng-template>
|
||||
}
|
||||
</pi-carousel>
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section
|
||||
title="Stepper"
|
||||
hint="Cubre WrapperTypeClass.Steps, que estaba declarado sin componente ni CSS."
|
||||
>
|
||||
<div class="stack full">
|
||||
<pi-stepper
|
||||
#stepper
|
||||
[steps]="steps"
|
||||
[(selectedIndex)]="stepperIndex"
|
||||
(stepChange)="log('paso ' + $event)"
|
||||
/>
|
||||
<div class="inline">
|
||||
<button pi-ghost small (click)="stepper.previous()">Anterior</button>
|
||||
<button pi-ghost small (click)="stepper.next()">Siguiente</button>
|
||||
</div>
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section
|
||||
title="Dynamic menu"
|
||||
hint="ComponentFamilyClass ya declaraba la familia dynamic-menu y util/menu traía la clase base; faltaba el componente."
|
||||
>
|
||||
<div class="stack full">
|
||||
<pi-dynamic-menu
|
||||
label="Navegación"
|
||||
[menuItems]="menuEntries"
|
||||
(selectEntry)="log('menú: ' + $any($event).label)"
|
||||
/>
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section
|
||||
title="Skeleton"
|
||||
hint="Resuelve el TODO que quedaba en pi-data-grid; también es el respaldo cuando no se pasa plantilla."
|
||||
>
|
||||
<div class="stack full">
|
||||
<pi-skeleton [lines]="3" />
|
||||
<pi-skeleton shape="block" width="100%" height="4rem" />
|
||||
<pi-skeleton shape="circle" width="3rem" />
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section
|
||||
@@ -130,5 +593,14 @@
|
||||
(changePage)="log('página ' + $event)"
|
||||
/>
|
||||
</app-section>
|
||||
|
||||
<app-section
|
||||
title="Página no encontrada"
|
||||
hint="pi-not-found: página completa (pi-headline + botón volver), no un widget en línea."
|
||||
>
|
||||
<div class="not-found-frame">
|
||||
<pi-not-found />
|
||||
</div>
|
||||
</app-section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -1,36 +1,16 @@
|
||||
:host {
|
||||
display: block;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem 4rem;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #5c82e9;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.5rem 0 0;
|
||||
color: #6b7280;
|
||||
max-width: 60ch;
|
||||
}
|
||||
|
||||
.event {
|
||||
display: inline-block;
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
background: #eef2ff;
|
||||
color: #4c5fd7;
|
||||
font-size: 0.8rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.event {
|
||||
display: inline-block;
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
background: var(--pg-badge-bg);
|
||||
color: var(--pg-badge-text);
|
||||
font-size: 0.8rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
main {
|
||||
@@ -67,16 +47,47 @@ main {
|
||||
.state {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: #374151;
|
||||
color: var(--pg-state-text);
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 0.9rem;
|
||||
border-radius: 8px;
|
||||
background: #0f172a;
|
||||
color: #cbd5f5;
|
||||
background: var(--pg-code-bg);
|
||||
color: var(--pg-code-text);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.range-slider-caption {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
width: 100%;
|
||||
|
||||
> span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--pg-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 4rem;
|
||||
border-radius: 8px;
|
||||
background: var(--pg-badge-bg);
|
||||
color: var(--pg-badge-text);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.not-found-frame {
|
||||
width: 100%;
|
||||
max-height: 22rem;
|
||||
overflow: auto;
|
||||
border: 1px dashed var(--pg-card-border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
FormField,
|
||||
email,
|
||||
@@ -7,43 +12,118 @@ import {
|
||||
min,
|
||||
required,
|
||||
} from '@angular/forms/signals';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
|
||||
import {
|
||||
PiAccordionComponent,
|
||||
PiAccordionItemContentDirective,
|
||||
PiAccordionItemDirective,
|
||||
PiActionButtonComponent,
|
||||
PiBannerComponent,
|
||||
PiBaseDataTableComponent,
|
||||
PiCardBottomContentDirective,
|
||||
PiCardCenterContentDirective,
|
||||
PiCardComponent,
|
||||
PiCardTopContentDirective,
|
||||
PiCarouselComponent,
|
||||
PiCarouselItemDirective,
|
||||
PiCellDirective,
|
||||
PiCellTypeComponent,
|
||||
PiChartComponent,
|
||||
PiChevronButtonComponent,
|
||||
PiChipActionComponent,
|
||||
PiChipComponent,
|
||||
PiChipDateComponent,
|
||||
PiChipStatusComponent,
|
||||
PiChipTimeComponent,
|
||||
PiChipValueComponent,
|
||||
PiCloseButtonComponent,
|
||||
PiColorPrimaryDirective,
|
||||
PiCurrencyFieldComponent,
|
||||
PiDataGridComponent,
|
||||
PiDataTableItemComponent,
|
||||
Dataset,
|
||||
DatasetColor,
|
||||
Device,
|
||||
PiDividerComponent,
|
||||
PiEmailFieldComponent,
|
||||
PiExpansionPanelComponent,
|
||||
PiGhostButtonDirective,
|
||||
PiHamburgerComponent,
|
||||
PiHeadlineComponent,
|
||||
PiHeadlineDescriptionContentDirective,
|
||||
PiHeadlineLabelContentDirective,
|
||||
PiIconButtonComponent,
|
||||
PiIconComponent,
|
||||
PiIconFieldComponent,
|
||||
PiIncrementalNumberFieldComponent,
|
||||
PiIncrementalRangeFieldComponent,
|
||||
PiLabelDirective,
|
||||
PiLargeButtonDirective,
|
||||
PiListItemComponent,
|
||||
PiListItemLeftContentDirective,
|
||||
PiListItemLeftIconDirective,
|
||||
PiListItemRightContentDirective,
|
||||
PiListItemRightIconDirective,
|
||||
ModalConfigKeys,
|
||||
ModalStyleType,
|
||||
ModalType,
|
||||
NotificationService,
|
||||
PiModalComponent,
|
||||
PiNoteActionContentDirective,
|
||||
PiNoteComponent,
|
||||
PiNotFoundComponent,
|
||||
getModalConfigs,
|
||||
PiNumberFieldComponent,
|
||||
PiOutlineButtonDirective,
|
||||
PiPaginatorComponent,
|
||||
PiPanelBottomContentDirective,
|
||||
PiPanelCenterContentDirective,
|
||||
PiPanelComponent,
|
||||
PiPanelExpandableContentDirective,
|
||||
PiPasswordFieldComponent,
|
||||
PiPhoneFieldComponent,
|
||||
PiProgressBarComponent,
|
||||
PiRadioButtonComponent,
|
||||
PiRange,
|
||||
PiRangeSliderComponent,
|
||||
PiSearchFieldComponent,
|
||||
PiSelectCarouselComponent,
|
||||
PiSelectComponent,
|
||||
PiSelectMultipleComponent,
|
||||
PiSelectOptionComponent,
|
||||
PiSelectPeriodComponent,
|
||||
PiSlideToggleComponent,
|
||||
PiSliderComponent,
|
||||
PiSmallButtonDirective,
|
||||
PiProgressSpinnerComponent,
|
||||
PiSwitchButtonComponent,
|
||||
PiTabComponent,
|
||||
PiTabContentDirective,
|
||||
TabButton,
|
||||
PiTextFieldComponent,
|
||||
PiTextViewerComponent,
|
||||
PiTextareaComponent,
|
||||
PiTileComponent,
|
||||
PiToggleButtonComponent,
|
||||
PiTooltipDirective,
|
||||
PiW100ButtonDirective,
|
||||
PiCheckboxComponent,
|
||||
PiRadioGroupComponent,
|
||||
PiDateFieldComponent,
|
||||
PiFileUploadComponent,
|
||||
PiDynamicMenuComponent,
|
||||
PiSkeletonComponent,
|
||||
PiStepperComponent,
|
||||
} from '@pi/ui';
|
||||
|
||||
import { ShowcaseSection } from './showcase-section';
|
||||
import { ShowcaseSection } from '../showcase-section';
|
||||
|
||||
interface SelectOptionValue {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface DemoModel {
|
||||
name: string;
|
||||
@@ -57,13 +137,26 @@ interface DemoModel {
|
||||
volume: number;
|
||||
newsletter: boolean;
|
||||
search: string;
|
||||
website: string;
|
||||
phone: string;
|
||||
weight: number | null;
|
||||
priceRange: PiRange;
|
||||
contactEmail: string;
|
||||
country: SelectOptionValue | null;
|
||||
tags: SelectOptionValue[];
|
||||
period: SelectOptionValue | null;
|
||||
plan: SelectOptionValue | null;
|
||||
terms: boolean;
|
||||
shipping: string | null;
|
||||
birthDate: Date | null;
|
||||
attachments: File[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
selector: 'app-catalog',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.scss',
|
||||
templateUrl: './catalog.page.html',
|
||||
styleUrl: './catalog.page.scss',
|
||||
imports: [
|
||||
FormField,
|
||||
ShowcaseSection,
|
||||
@@ -75,10 +168,14 @@ interface DemoModel {
|
||||
PiIconButtonComponent,
|
||||
PiGhostButtonDirective,
|
||||
PiOutlineButtonDirective,
|
||||
PiColorPrimaryDirective,
|
||||
PiLabelDirective,
|
||||
PiLargeButtonDirective,
|
||||
PiSmallButtonDirective,
|
||||
PiW100ButtonDirective,
|
||||
PiRadioButtonComponent,
|
||||
PiSwitchButtonComponent,
|
||||
PiToggleButtonComponent,
|
||||
// Contenido
|
||||
PiIconComponent,
|
||||
PiTextViewerComponent,
|
||||
@@ -87,6 +184,19 @@ interface DemoModel {
|
||||
PiChipStatusComponent,
|
||||
PiChipValueComponent,
|
||||
PiChipActionComponent,
|
||||
PiChipTimeComponent,
|
||||
PiChipDateComponent,
|
||||
PiTileComponent,
|
||||
PiHeadlineComponent,
|
||||
PiHeadlineLabelContentDirective,
|
||||
PiHeadlineDescriptionContentDirective,
|
||||
PiListItemComponent,
|
||||
PiListItemLeftIconDirective,
|
||||
PiListItemLeftContentDirective,
|
||||
PiListItemRightContentDirective,
|
||||
PiListItemRightIconDirective,
|
||||
PiTooltipDirective,
|
||||
PiNotFoundComponent,
|
||||
// Feedback
|
||||
PiProgressSpinnerComponent,
|
||||
PiProgressBarComponent,
|
||||
@@ -102,9 +212,54 @@ interface DemoModel {
|
||||
PiSearchFieldComponent,
|
||||
PiSlideToggleComponent,
|
||||
PiSliderComponent,
|
||||
PiRangeSliderComponent,
|
||||
PiIconFieldComponent,
|
||||
PiPhoneFieldComponent,
|
||||
PiNumberFieldComponent,
|
||||
// Contenedores
|
||||
PiCardComponent,
|
||||
PiCardTopContentDirective,
|
||||
PiCardCenterContentDirective,
|
||||
PiCardBottomContentDirective,
|
||||
PiPanelComponent,
|
||||
PiPanelCenterContentDirective,
|
||||
PiPanelBottomContentDirective,
|
||||
PiExpansionPanelComponent,
|
||||
PiPanelExpandableContentDirective,
|
||||
PiAccordionComponent,
|
||||
PiAccordionItemDirective,
|
||||
PiAccordionItemContentDirective,
|
||||
PiTabComponent,
|
||||
PiTabContentDirective,
|
||||
PiBannerComponent,
|
||||
PiNoteComponent,
|
||||
PiNoteActionContentDirective,
|
||||
// Selects
|
||||
PiSelectComponent,
|
||||
PiSelectMultipleComponent,
|
||||
PiSelectPeriodComponent,
|
||||
PiSelectCarouselComponent,
|
||||
PiSelectOptionComponent,
|
||||
// Datos
|
||||
PiDataGridComponent,
|
||||
PiCellDirective,
|
||||
PiBaseDataTableComponent,
|
||||
PiDataTableItemComponent,
|
||||
PiCellTypeComponent,
|
||||
PiChartComponent,
|
||||
PiCarouselComponent,
|
||||
PiCarouselItemDirective,
|
||||
// Componentes nuevos
|
||||
PiCheckboxComponent,
|
||||
PiRadioGroupComponent,
|
||||
PiDateFieldComponent,
|
||||
PiFileUploadComponent,
|
||||
PiDynamicMenuComponent,
|
||||
PiSkeletonComponent,
|
||||
PiStepperComponent,
|
||||
],
|
||||
})
|
||||
export class App {
|
||||
export class CatalogPage {
|
||||
/* --- Signal Forms ----------------------------------------------------- */
|
||||
|
||||
private readonly model = signal<DemoModel>({
|
||||
@@ -119,6 +274,19 @@ export class App {
|
||||
volume: 40,
|
||||
newsletter: false,
|
||||
search: '',
|
||||
website: '',
|
||||
phone: '',
|
||||
weight: null,
|
||||
priceRange: { from: 20, to: 80 },
|
||||
contactEmail: '',
|
||||
country: null,
|
||||
tags: [],
|
||||
period: null,
|
||||
plan: null,
|
||||
terms: false,
|
||||
shipping: null,
|
||||
birthDate: null,
|
||||
attachments: [],
|
||||
});
|
||||
|
||||
protected readonly f = form(this.model, (m) => {
|
||||
@@ -131,12 +299,155 @@ export class App {
|
||||
/** Se muestra en el panel lateral para ver el modelo en vivo. */
|
||||
protected readonly modelJson = () => JSON.stringify(this.model(), null, 2);
|
||||
|
||||
protected readonly countries = [
|
||||
{ id: 1, name: 'España' },
|
||||
{ id: 2, name: 'Portugal' },
|
||||
{ id: 3, name: 'Francia' },
|
||||
];
|
||||
|
||||
protected readonly tags = [
|
||||
{ id: 1, name: 'Urgente' },
|
||||
{ id: 2, name: 'Facturación' },
|
||||
{ id: 3, name: 'Soporte' },
|
||||
{ id: 4, name: 'Ventas' },
|
||||
];
|
||||
|
||||
protected readonly plans = [
|
||||
{ id: 1, name: 'Básico', description: '9 €/mes' },
|
||||
{ id: 2, name: 'Pro', description: '29 €/mes' },
|
||||
{ id: 3, name: 'Empresas', description: '99 €/mes' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Los campos ya calculan sus errores en cuanto se marcan como `touched`
|
||||
* (así lo exige `ErrorStateMatcher` de Material); antes de eso el usuario
|
||||
* no ha interactuado y no tiene sentido mostrarlos. Este botón simula el
|
||||
* envío del formulario para revelarlos todos de golpe.
|
||||
*/
|
||||
protected submitForm(): void {
|
||||
this.f().markAsTouched();
|
||||
this.log('formulario enviado');
|
||||
}
|
||||
|
||||
/* --- Estado de los demás componentes ---------------------------------- */
|
||||
|
||||
protected readonly currentPage = signal(1);
|
||||
protected readonly lastEvent = signal('—');
|
||||
|
||||
protected readonly radioActive = signal(false);
|
||||
protected readonly switchActive = signal(true);
|
||||
protected readonly toggleActive = signal(false);
|
||||
protected readonly isPanelCollapsed = signal(false);
|
||||
protected readonly expandedAccordionIndex = signal<number | null>(0);
|
||||
|
||||
protected readonly tabs: TabButton[] = [
|
||||
{ label: 'General' },
|
||||
{ label: 'Facturación', icon: 'euro' },
|
||||
{ label: 'Avanzado', isDisabled: true },
|
||||
];
|
||||
|
||||
protected readonly shippingOptions = [
|
||||
{ value: 'standard', label: 'Estándar (3-5 días)' },
|
||||
{ value: 'express', label: 'Exprés (24 h)' },
|
||||
{ value: 'pickup', label: 'Recoger en tienda', disabled: true },
|
||||
];
|
||||
|
||||
protected readonly stepperIndex = signal(1);
|
||||
|
||||
protected readonly steps = [
|
||||
{ label: 'Cuenta', description: 'Datos de acceso' },
|
||||
{ label: 'Perfil', description: 'Información personal' },
|
||||
{ label: 'Pago', description: 'Método de cobro' },
|
||||
];
|
||||
|
||||
protected readonly menuEntries = [
|
||||
{ label: 'Panel', icon: 'dashboard', isActive: true },
|
||||
{
|
||||
label: 'Operaciones',
|
||||
icon: 'briefcase',
|
||||
entries: [
|
||||
{ label: 'En curso', icon: 'time-detail' },
|
||||
{ label: 'Cerradas', icon: 'check' },
|
||||
],
|
||||
},
|
||||
{ label: 'Ajustes', icon: 'user', isDisabled: true },
|
||||
];
|
||||
|
||||
protected log(what: string): void {
|
||||
this.lastEvent.set(`${what} · ${new Date().toLocaleTimeString('es-ES')}`);
|
||||
}
|
||||
|
||||
/* --- Modal y notificaciones --------------------------------------------- */
|
||||
|
||||
/**
|
||||
* `pi-modal` y `pi-snack-bar` no se usan como etiquetas en una plantilla:
|
||||
* viven detrás de `MatDialog`/`MatSnackBar` y se abren desde TypeScript.
|
||||
*/
|
||||
private readonly dialog = inject(MatDialog);
|
||||
private readonly notification = inject(NotificationService);
|
||||
|
||||
protected openDemoModal(): void {
|
||||
this.dialog.open(
|
||||
PiModalComponent,
|
||||
getModalConfigs(ModalConfigKeys.Small, {
|
||||
type: ModalType.DIALOG,
|
||||
pathImg: '404.svg',
|
||||
label: 'Confirmar acción',
|
||||
description: '¿Seguro que quieres continuar?',
|
||||
style: ModalStyleType.PRIMARY,
|
||||
buttonText: { primaryAction: 'Aceptar', secondaryAction: 'Cancelar' },
|
||||
}),
|
||||
);
|
||||
this.log('modal abierto');
|
||||
}
|
||||
|
||||
protected showSuccessNotification(): void {
|
||||
this.notification.success(
|
||||
'Guardado correctamente',
|
||||
'Los cambios ya están disponibles',
|
||||
);
|
||||
}
|
||||
|
||||
protected showErrorNotification(): void {
|
||||
this.notification.error(
|
||||
'Ha ocurrido un error',
|
||||
'Inténtalo de nuevo más tarde',
|
||||
);
|
||||
}
|
||||
|
||||
/* --- Datos: grid, tabla, gráfico, carrusel ------------------------------ */
|
||||
|
||||
protected readonly employees = [
|
||||
{ name: 'Ana López', role: 'Diseño' },
|
||||
{ name: 'Luis Marín', role: 'Backend' },
|
||||
{ name: 'Marta Ruiz', role: 'Ventas' },
|
||||
];
|
||||
|
||||
protected readonly invoiceRows = [
|
||||
['Ana López', '150,00 €', '12/07/2026'],
|
||||
['Luis Marín', '89,50 €', '15/07/2026'],
|
||||
];
|
||||
|
||||
protected readonly device: Device = 'DESKTOP';
|
||||
|
||||
protected readonly chartDatasets: Dataset[] = [
|
||||
{
|
||||
label: 'Ingresos',
|
||||
data: [120, 190, 150, 220],
|
||||
borderColor: DatasetColor.primary,
|
||||
backgroundColor: DatasetColor.primary,
|
||||
cubicInterpolationMode: 'monotone',
|
||||
unit: ' €',
|
||||
unitPosition: 'after',
|
||||
},
|
||||
];
|
||||
|
||||
protected readonly carouselItems = [
|
||||
{ id: 1, label: 'Elemento 1' },
|
||||
{ id: 2, label: 'Elemento 2' },
|
||||
{ id: 3, label: 'Elemento 3' },
|
||||
{ id: 4, label: 'Elemento 4' },
|
||||
{ id: 5, label: 'Elemento 5' },
|
||||
{ id: 6, label: 'Elemento 6' },
|
||||
];
|
||||
}
|
||||
|
||||
82
apps/playground/src/app/pages/layouts/layouts.page.html
Normal file
82
apps/playground/src/app/pages/layouts/layouts.page.html
Normal file
@@ -0,0 +1,82 @@
|
||||
<app-section
|
||||
title="Cómo se usan"
|
||||
hint="El layout es una clase en el contenedor de la vista, normalmente vía @HostBinding('class') con una constante de LayoutTypeClass. Casi todos esperan un .pi-container-layout como padre."
|
||||
>
|
||||
<pre class="snippet">
|
||||
@Component({{ '{' }} ... {{ '}' }})
|
||||
export class MiVista {{ '{' }}
|
||||
@HostBinding('class') cls = LayoutTypeClass.VerticallyCentered;
|
||||
{{ '}' }}</pre>
|
||||
</app-section>
|
||||
|
||||
<app-section
|
||||
title="Autónomos"
|
||||
hint="Su CSS actúa sobre el propio contenedor, así que se ven con bloques sueltos. Las vistas van recortadas porque algunos se dimensionan en vh."
|
||||
>
|
||||
<label class="clip-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="clipped()"
|
||||
(change)="clipped.set($any($event.target).checked)"
|
||||
/>
|
||||
<span>Recortar vistas previas</span>
|
||||
</label>
|
||||
</app-section>
|
||||
|
||||
@for (layout of standalone; track layout.classes) {
|
||||
<app-section [title]="layout.name" [hint]="layout.description">
|
||||
<div class="demo">
|
||||
<code class="demo__classes">{{ layout.classes }}</code>
|
||||
|
||||
<div class="demo__frame" [class.is-clipped]="clipped()">
|
||||
<div class="pi-container-layout">
|
||||
<div [class]="layout.classes">
|
||||
@for (block of layout.blocks; track block) {
|
||||
<div class="demo__block">{{ block }}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</app-section>
|
||||
}
|
||||
|
||||
<app-section
|
||||
title="Dependen de composición"
|
||||
hint="Estos no cambian nada con bloques sueltos: su CSS apunta a los componentes de wrapper o a estados del shell. Se listan con lo que necesitan alrededor para hacer algo."
|
||||
>
|
||||
<div class="needs">
|
||||
@for (layout of compositional; track layout.classes) {
|
||||
<div class="needs__item">
|
||||
<strong>{{ layout.name }}</strong>
|
||||
<code>{{ layout.classes }}</code>
|
||||
<p>{{ layout.needs }}</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section
|
||||
title="Alturas"
|
||||
hint="Se combinan con cualquier layout para fijar una altura mínima. Hay variantes por dispositivo con heights(mobile, tablet, desktop)."
|
||||
>
|
||||
<div class="heights">
|
||||
@for (h of heights; track h.cls) {
|
||||
<div class="heights__item">
|
||||
<code>{{ h.cls }}</code>
|
||||
<span>{{ h.hint }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</app-section>
|
||||
|
||||
<app-section
|
||||
title="Sin CSS en la capa genérica"
|
||||
hint="Siguen expuestos en LayoutTypeClass, pero su CSS eran composiciones de app (estados del aside, tipos de usuario) que vivían en la carpeta de marca y no se han traído a common/."
|
||||
>
|
||||
<div class="missing">
|
||||
<code>pi-layout__00-authentication</code>
|
||||
<code>pi-layout__01-dashboard</code>
|
||||
<code>pi-layout__05a-splitted-paywall</code>
|
||||
</div>
|
||||
</app-section>
|
||||
125
apps/playground/src/app/pages/layouts/layouts.page.scss
Normal file
125
apps/playground/src/app/pages/layouts/layouts.page.scss
Normal file
@@ -0,0 +1,125 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.snippet {
|
||||
margin: 0;
|
||||
padding: 0.9rem;
|
||||
border-radius: 8px;
|
||||
background: var(--pg-code-bg);
|
||||
color: var(--pg-code-text);
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.5;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.clip-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
color: var(--pg-text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.demo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.6rem;
|
||||
width: 100%;
|
||||
|
||||
&__classes {
|
||||
align-self: flex-start;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
background: var(--pg-badge-bg);
|
||||
color: var(--pg-badge-text);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
// Marco que aísla la vista previa: el layout real se dimensiona dentro.
|
||||
&__frame {
|
||||
width: 100%;
|
||||
border: 1px dashed var(--pg-card-border);
|
||||
border-radius: 8px;
|
||||
overflow: auto;
|
||||
resize: vertical;
|
||||
|
||||
&.is-clipped {
|
||||
max-height: 16rem;
|
||||
}
|
||||
}
|
||||
|
||||
&__block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 3rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: var(--pg-badge-bg);
|
||||
color: var(--pg-badge-text);
|
||||
font-size: 0.8rem;
|
||||
// Marca los huecos que reparte el layout.
|
||||
outline: 1px solid color-mix(in srgb, currentColor 25%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.heights,
|
||||
.missing {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
|
||||
code {
|
||||
padding: 0.2rem 0.55rem;
|
||||
border-radius: 4px;
|
||||
background: var(--pg-badge-bg);
|
||||
color: var(--pg-badge-text);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
}
|
||||
|
||||
.heights__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
span {
|
||||
font-size: 0.75rem;
|
||||
color: var(--pg-text-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.needs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
|
||||
&__item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.3rem;
|
||||
padding-left: 0.75rem;
|
||||
border-left: 2px solid var(--pg-card-border);
|
||||
|
||||
strong {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
code {
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
background: var(--pg-badge-bg);
|
||||
color: var(--pg-badge-text);
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
font-size: 0.82rem;
|
||||
color: var(--pg-text-muted);
|
||||
max-width: 70ch;
|
||||
}
|
||||
}
|
||||
}
|
||||
117
apps/playground/src/app/pages/layouts/layouts.page.ts
Normal file
117
apps/playground/src/app/pages/layouts/layouts.page.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
|
||||
|
||||
import { LayoutHeight, LayoutTypeClass } from '@pi/ui';
|
||||
|
||||
import { ShowcaseSection } from '../showcase-section';
|
||||
|
||||
interface LayoutDemo {
|
||||
/** Nombre corto para el título del apartado. */
|
||||
name: string;
|
||||
/** Constante de `LayoutTypeClass`, tal cual se aplica en el HostBinding. */
|
||||
classes: string;
|
||||
/** Qué hace realmente el CSS de este layout. */
|
||||
description: string;
|
||||
/** Bloques de relleno que se pintan dentro de la vista previa. */
|
||||
blocks: string[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-layouts',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './layouts.page.html',
|
||||
styleUrl: './layouts.page.scss',
|
||||
imports: [ShowcaseSection],
|
||||
})
|
||||
export class LayoutsPage {
|
||||
/**
|
||||
* Layouts que se sostienen solos: su CSS actúa sobre el propio contenedor,
|
||||
* así que la vista previa con bloques sueltos ya enseña lo que hacen.
|
||||
*/
|
||||
protected readonly standalone: LayoutDemo[] = [
|
||||
{
|
||||
name: 'Vertically centered',
|
||||
classes: LayoutTypeClass.VerticallyCentered,
|
||||
description:
|
||||
'Columna flex centrada verticalmente (justify-content: center). Típico de login, errores o estados vacíos.',
|
||||
blocks: ['Bloque centrado'],
|
||||
},
|
||||
{
|
||||
name: 'Space between',
|
||||
classes: LayoutTypeClass.SpaceBetween,
|
||||
description:
|
||||
'Columna flex con justify-content: space-between: empuja el primer bloque arriba y el último abajo.',
|
||||
blocks: ['Cabecera', 'Pie'],
|
||||
},
|
||||
{
|
||||
name: 'Full width auto',
|
||||
classes: LayoutTypeClass.FullWidthAuto,
|
||||
description:
|
||||
'Añade margin: auto sobre el full width, para centrar el bloque cuando no ocupa todo el ancho.',
|
||||
blocks: ['Contenido centrado'],
|
||||
},
|
||||
{
|
||||
name: 'Pricing',
|
||||
classes: LayoutTypeClass.Pricing,
|
||||
description:
|
||||
'Rejilla de 3 columnas (repeat(3, 1fr)) para tarifas; tiene variante 1fr 3fr con tarjeta destacada.',
|
||||
blocks: ['Básico', 'Pro', 'Empresas'],
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Estos no se pueden enseñar con bloques sueltos: su CSS apunta a los
|
||||
* componentes de wrapper o a estados del shell de la aplicación, así que
|
||||
* fuera de esa composición no cambian nada visible.
|
||||
*/
|
||||
protected readonly compositional = [
|
||||
{
|
||||
name: 'Full width',
|
||||
classes: LayoutTypeClass.FullWidth,
|
||||
needs:
|
||||
'Un ancestro con `aside-is-open`: los anchos máximos se calculan a partir del aside y el navbar.',
|
||||
},
|
||||
{
|
||||
name: 'Splitted',
|
||||
classes: LayoutTypeClass.Splitted,
|
||||
needs:
|
||||
'No parte en dos por sí solo: es una columna a 100vh y las mitades las aportan los slots izquierdo/derecho de `pi-main-wrapper`.',
|
||||
},
|
||||
{
|
||||
name: 'Edge to edge',
|
||||
classes: LayoutTypeClass.EdgeToEdge,
|
||||
needs:
|
||||
'Un `pi-wrapper__header` como hijo dentro de `.pi-container-layout`: lo que ajusta es su espaciado superior.',
|
||||
},
|
||||
{
|
||||
name: 'List',
|
||||
classes: LayoutTypeClass.List,
|
||||
needs:
|
||||
'Wrappers (`pi-wrapper`, `pi-wrapper__list`) y un `pi-data-grid`: limita sus anchos y centra la rejilla.',
|
||||
},
|
||||
{
|
||||
name: 'List auto',
|
||||
classes: LayoutTypeClass.ListAuto,
|
||||
needs: 'Igual que List, para listados con scroll infinito.',
|
||||
},
|
||||
{
|
||||
name: 'Form',
|
||||
classes: LayoutTypeClass.Form,
|
||||
needs:
|
||||
'Ir anidado dentro de un `pi-layout__05-splitted`: solo ahí recibe padding y scrollbar propios.',
|
||||
},
|
||||
];
|
||||
|
||||
/** Alturas mínimas, aplicables sobre cualquier layout. */
|
||||
protected readonly heights = [
|
||||
{ label: 'small', cls: LayoutHeight.Small, hint: '33.33vh' },
|
||||
{ label: 'medium', cls: LayoutHeight.Medium, hint: '40vh' },
|
||||
{ label: 'large', cls: LayoutHeight.Large, hint: '75vh' },
|
||||
{ label: 'full', cls: LayoutHeight.Full, hint: '100vh' },
|
||||
];
|
||||
|
||||
/**
|
||||
* Las vistas previas van recortadas: varios layouts piden `vh` y ocuparían
|
||||
* pantallas enteras. Se puede soltar el recorte para verlos a tamaño real.
|
||||
*/
|
||||
protected readonly clipped = signal(true);
|
||||
}
|
||||
@@ -17,14 +17,14 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
`,
|
||||
styles: `
|
||||
section {
|
||||
background: #fff;
|
||||
border: 1px solid #e7e9ee;
|
||||
background: var(--pg-card-bg);
|
||||
border: 1px solid var(--pg-card-border);
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
header {
|
||||
border-bottom: 1px solid #eef0f4;
|
||||
border-bottom: 1px solid var(--pg-card-header-border);
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
@@ -34,12 +34,12 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
color: #5c82e9;
|
||||
color: var(--pg-heading);
|
||||
}
|
||||
p {
|
||||
margin: 0.35rem 0 0;
|
||||
font-size: 0.85rem;
|
||||
color: #6b7280;
|
||||
color: var(--pg-text-muted);
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="is-desktop">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>playground</title>
|
||||
@@ -7,7 +7,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Valor inicial; App lo reescribe según el tema elegido (ver ShellClass). -->
|
||||
<body class="pi-shell pi-shell__default">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -13,6 +13,14 @@
|
||||
@use '../../../libs/ui/src/lib/styles/common/components' as *;
|
||||
@use '../../../libs/ui/src/lib/styles/common/layouts' as *;
|
||||
@use '../../../libs/ui/src/lib/styles/common/wrappers' as *;
|
||||
// Overrides de Material (campos, checkbox, diálogos, sliders, menús...). Es lo
|
||||
// que hace que los componentes se vean «pi» y no Material de serie; el
|
||||
// `@import` de `@typeform/embed` que estorbaba está en `common/index.scss`, no
|
||||
// aquí, así que este partial sí se puede cargar.
|
||||
@use '../../../libs/ui/src/lib/styles/common/vendors' as *;
|
||||
// Capa de temas: declara los tokens --pi-theme-* que consume `common/`.
|
||||
// Va después de `common` para que sus overrides ganen.
|
||||
@use '../../../libs/ui/src/lib/styles/themes' as *;
|
||||
|
||||
html {
|
||||
@include mat.theme(
|
||||
@@ -27,6 +35,90 @@ html {
|
||||
);
|
||||
|
||||
color-scheme: light;
|
||||
|
||||
// Chrome propio del playground (cabecera, tarjetas de sección, bloque de
|
||||
// estado). Los componentes de @pi/ui usan --pi-color-*, sobrescritas más
|
||||
// abajo para el modo oscuro.
|
||||
--pg-page-bg: #f7f8fa;
|
||||
--pg-page-text: #1c1c1e;
|
||||
--pg-heading: #5c82e9;
|
||||
--pg-text-muted: #6b7280;
|
||||
--pg-badge-bg: #eef2ff;
|
||||
--pg-badge-text: #4c5fd7;
|
||||
--pg-card-bg: #fff;
|
||||
--pg-card-border: #e7e9ee;
|
||||
--pg-card-header-border: #eef0f4;
|
||||
--pg-state-text: #374151;
|
||||
--pg-code-bg: #0f172a;
|
||||
--pg-code-text: #cbd5f5;
|
||||
}
|
||||
|
||||
// Modo oscuro: se activa añadiendo la clase `dark-mode` al <html> (ver
|
||||
// App.darkMode en apps/playground/src/app/app.ts). @pi/ui no está diseñada
|
||||
// con un tema oscuro propio, así que esto es una reasignación best-effort de
|
||||
// sus variables --pi-color-* neutras (blancos/negros/grises/fondos); las
|
||||
// paletas de marca (primary/accent/warn/success/highlighted) se mantienen
|
||||
// igual y pueden perder algo de contraste en algunos componentes.
|
||||
html.dark-mode {
|
||||
@include mat.theme(
|
||||
(
|
||||
color: (
|
||||
theme-type: dark,
|
||||
primary: mat.$azure-palette,
|
||||
tertiary: mat.$blue-palette,
|
||||
),
|
||||
typography: Roboto,
|
||||
density: 0,
|
||||
)
|
||||
);
|
||||
|
||||
color-scheme: dark;
|
||||
|
||||
--pg-page-bg: #14161a;
|
||||
--pg-page-text: #e5e7eb;
|
||||
--pg-heading: #8cacf2;
|
||||
--pg-text-muted: #9aa1ac;
|
||||
--pg-badge-bg: #1f2a4d;
|
||||
--pg-badge-text: #b9c4ff;
|
||||
--pg-card-bg: #1c1f26;
|
||||
--pg-card-border: #2a2f3a;
|
||||
--pg-card-header-border: #262b35;
|
||||
--pg-state-text: #cbd2db;
|
||||
--pg-code-bg: #0a0d16;
|
||||
--pg-code-text: #cbd5f5;
|
||||
|
||||
// --- @pi/ui: neutros -------------------------------------------------
|
||||
--pi-color-white: #1c1f26;
|
||||
--pi-color-white-900: #252932;
|
||||
--pi-color-black: #f1f2f4;
|
||||
--pi-color-black-200: #c7cbd4;
|
||||
|
||||
--pi-color-grey-10: #111318;
|
||||
--pi-color-grey-25: #161920;
|
||||
--pi-color-grey-50: #1b1e26;
|
||||
--pi-color-grey-75: #20232c;
|
||||
--pi-color-grey-100: #2a2e38;
|
||||
--pi-color-grey-200: #3a3f4b;
|
||||
--pi-color-grey-300: #565c6b;
|
||||
--pi-color-grey-400: #7a8190;
|
||||
--pi-color-grey: #9aa1ac;
|
||||
--pi-color-grey-900: #e4e6ea;
|
||||
|
||||
--pi-color-background-300: #4b505c;
|
||||
--pi-color-background-400: #1c1f26;
|
||||
--pi-color-background-500: #181b22;
|
||||
}
|
||||
|
||||
// El tema alternativo usa escalones claros de su marca como superficie de los
|
||||
// campos (`--pi-theme-field-surface`). Como el bloque de arriba solo reasigna
|
||||
// neutros, en oscuro esos campos se quedaban en claro: aquí se oscurecen los
|
||||
// pocos escalones que actúan de superficie, manteniendo el tinte violeta.
|
||||
html.dark-mode .pi-shell__alternative {
|
||||
--pi-color-primary-10: #16131f;
|
||||
--pi-color-primary-25: #1a1626;
|
||||
--pi-color-primary-50: #201a30;
|
||||
--pi-color-primary-75: #2a2140;
|
||||
--pi-color-primary-100: #3a2d5c;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -36,6 +128,6 @@ body {
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
background: #f7f8fa;
|
||||
color: #1c1c1e;
|
||||
background: var(--pg-page-bg);
|
||||
color: var(--pg-page-text);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user