first commit

This commit is contained in:
2026-10-03 13:40:26 +02:00
parent 732a5b5a16
commit 566f415fa7
555 changed files with 62704 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
# web-shared-ui
Integración de `@pi/ui` y piezas de interfaz comunes.
- **Import**: `@task-stream/web/shared/ui`
- **Capa**: `presenters` · **Tags**: `scope:web-shared`, `type:presenters`
## Contenido
- `providePiUi()`: animaciones, iconos, textos de la librería, tema y modo oscuro.
- `icons.ts`: iconos SVG en línea, incluidos los que usa la propia librería.
- `AppearanceService` (modo oscuro), `ToastService` (`pi-snack-bar`), `DrawerService` (`pi-drawer`), `AppShellComponent`
(la cabecera de pireon.dev: logo, «Pireon | TaskStream», luces de fondo) y `LogoComponent` (el logo de pireon.dev,
en `apps/web/public`).
Particularidades de `@pi/ui`: [Cliente web](../../../../docs/04-cliente-web.md#44-piui).
## Comandos
```sh
npx nx lint web-shared-ui
```
Reglas de dependencia entre capas: [Arquitectura](../../../../docs/02-arquitectura.md#21-capas-hexagonales).

View File

@@ -0,0 +1,34 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../../../eslint.config.mjs';
export default [
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
...baseConfig,
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'ts',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'ts',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];

View File

@@ -0,0 +1,13 @@
{
"name": "web-shared-ui",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/web/shared/ui/src",
"prefix": "ts",
"projectType": "library",
"tags": ["scope:web-shared", "type:presenters"],
"targets": {
"lint": {
"executor": "@nx/eslint:lint"
}
}
}

View File

@@ -0,0 +1,7 @@
export * from './lib/pi-ui.providers';
export * from './lib/appearance';
export * from './lib/toast.service';
export * from './lib/app-shell';
export * from './lib/icons';
export * from './lib/drawer.service';
export * from './lib/logo';

View File

@@ -0,0 +1,188 @@
:host {
display: flex;
flex-direction: column;
min-height: 100vh;
// The lights below spill sideways: clipped here (not `hidden`, which would
// make a scroll container and break the sticky header).
overflow-x: clip;
}
// Same bar as pireon.dev's site header.
.ts-header {
position: sticky;
top: 0;
z-index: 50;
background-color: color-mix(in srgb, var(--ts-surface) 88%, transparent);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--ts-line);
}
.ts-header__inner {
display: flex;
align-items: center;
gap: 1.5rem;
max-width: 96rem;
height: 4rem;
margin-inline: auto;
padding-inline: 1.25rem;
box-sizing: border-box;
@media (min-width: 768px) {
padding-inline: 2rem;
}
}
.ts-brand {
display: inline-flex;
align-items: center;
gap: 0.625rem;
color: var(--ts-ink);
text-decoration: none;
}
.ts-brand__platform {
font-weight: 700;
}
// The product, after the platform: "Pireon | TaskStream".
.ts-brand__product {
padding-left: 0.625rem;
border-left: 1px solid var(--ts-line-strong);
font-weight: 600;
background: linear-gradient(
100deg,
var(--pi-color-primary),
var(--pi-color-accent)
);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.ts-header__nav {
display: flex;
align-items: center;
gap: 0.25rem;
flex: 1;
min-width: 0;
overflow-x: auto;
scrollbar-width: none;
}
.ts-header__actions {
display: flex;
align-items: center;
gap: 0.75rem;
}
.ts-header__icon {
--ts-icon-button-size: 2.25rem;
--ts-icon-button-border: var(--ts-line);
color: var(--ts-muted);
transition:
color 0.15s ease,
background-color 0.15s ease;
&:hover {
color: var(--ts-ink);
background: var(--pi-color-primary-25);
}
}
// A shadow under the header once the page moves, as on pireon.dev.
@keyframes header-lift {
to {
border-bottom-color: transparent;
box-shadow: 0 10px 30px -18px
color-mix(in srgb, var(--pi-color-primary) 45%, transparent);
}
}
@supports (animation-timeline: scroll()) {
@media (prefers-reduced-motion: no-preference) {
.ts-header {
animation: header-lift linear both;
animation-timeline: scroll(root);
animation-range: 0 5rem;
}
}
}
// Content over the platform's two soft lights (pireon.dev's `glow`).
.ts-main {
position: relative;
isolation: isolate;
flex: 1;
width: 100%;
max-width: 96rem;
margin-inline: auto;
padding: 2.5rem 1.25rem 4rem;
box-sizing: border-box;
@media (min-width: 768px) {
padding-inline: 2rem;
}
&::before,
&::after {
content: '';
position: absolute;
z-index: -1;
border-radius: 50%;
pointer-events: none;
filter: blur(80px);
}
&::before {
top: -16rem;
left: 50%;
width: 64rem;
height: 34rem;
transform: translateX(-58%);
opacity: 0.4;
background: radial-gradient(
closest-side,
var(--pi-color-primary),
transparent
);
}
&::after {
top: -4rem;
right: -10rem;
width: 26rem;
height: 26rem;
opacity: 0.28;
background: radial-gradient(
closest-side,
var(--pi-color-accent),
transparent
);
}
}
@media (max-width: 640px) {
.ts-header__inner {
gap: 0.75rem;
}
.ts-brand__platform {
display: none;
}
.ts-brand__product {
padding-left: 0;
border-left: 0;
}
.ts-main {
padding-top: 1.5rem;
}
}
// On a phone the brand already leads to the projects list.
@media (max-width: 480px) {
.ts-header__nav {
visibility: hidden;
}
}

View File

@@ -0,0 +1,54 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { PiIconComponent } from '@pi/ui';
import { AppearanceService } from './appearance';
import { LogoComponent } from './logo';
/**
* Page frame, in the Pireon platform's look (same header as pireon.dev):
* translucent bar with the Pireon mark and the product name, navigation,
* appearance and user; the routed content over the platform's soft lights.
* Navigation and user menu are projected by the app.
*/
@Component({
selector: 'ts-app-shell',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RouterLink, PiIconComponent, LogoComponent],
styleUrl: './app-shell.scss',
template: `
<header class="ts-header">
<div class="ts-header__inner">
<a
class="ts-brand"
routerLink="/"
aria-label="Pireon TaskStream, inicio"
>
<ts-logo [size]="32" />
<span class="ts-brand__platform">Pireon</span>
<span class="ts-brand__product">TaskStream</span>
</a>
<nav class="ts-header__nav" aria-label="Principal">
<ng-content select="[shellNav]" />
</nav>
<div class="ts-header__actions">
<button
type="button"
class="ts-icon-button ts-header__icon"
[attr.aria-label]="appearance.dark() ? 'Modo claro' : 'Modo oscuro'"
[attr.title]="appearance.dark() ? 'Modo claro' : 'Modo oscuro'"
(click)="appearance.toggle()"
>
<pi-icon [icon]="appearance.dark() ? 'sun' : 'moon'" size="small" />
</button>
<ng-content select="[shellUser]" />
</div>
</div>
</header>
<main class="ts-main">
<ng-content />
</main>
`,
})
export class AppShellComponent {
protected readonly appearance = inject(AppearanceService);
}

View File

@@ -0,0 +1,46 @@
/**
* Dark mode, owned here rather than by `@pi/ui`.
*
* `PiThemeService` toggles `html.dark-mode` and persists the choice, but it
* reads its key back with `!!localStorage.getItem(key)` while writing the
* boolean as a string — so `"false"` reads back as dark. (Same workaround as
* landing-web.) This service keeps its own key and drives the library.
*/
import { computed, inject, Injectable } from '@angular/core';
import { PiThemeService } from '@pi/ui';
const APPEARANCE_KEY = 'task-stream.appearance';
@Injectable({ providedIn: 'root' })
export class AppearanceService {
private readonly theme = inject(PiThemeService);
readonly dark = computed(() => this.theme.darkMode());
restore(): void {
this.theme.setDarkMode(this.preferred());
}
toggle(): void {
this.set(!this.dark());
}
set(dark: boolean): void {
this.theme.setDarkMode(dark);
try {
localStorage.setItem(APPEARANCE_KEY, dark ? 'dark' : 'light');
} catch {
// Storage disabled: the choice lasts this visit only.
}
}
private preferred(): boolean {
try {
const stored = localStorage.getItem(APPEARANCE_KEY);
if (stored === 'dark' || stored === 'light') return stored === 'dark';
} catch {
// Fall through to the system preference.
}
return window.matchMedia?.('(prefers-color-scheme: dark)').matches ?? false;
}
}

View File

@@ -0,0 +1,24 @@
import { inject, Injectable, TemplateRef } from '@angular/core';
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { DrawerConfigKeys, getDrawerConfigs, PiDrawerComponent } from '@pi/ui';
/** Side panel for forms (new ticket, new release…) with the library's `pi-drawer`. */
@Injectable({ providedIn: 'root' })
export class DrawerService {
private readonly dialog = inject(MatDialog);
open(
title: string,
template: TemplateRef<unknown>,
width = '32rem',
): MatDialogRef<PiDrawerComponent> {
return this.dialog.open(PiDrawerComponent, {
...getDrawerConfigs(DrawerConfigKeys.Right, { title, template } as never),
width,
});
}
closeAll(): void {
this.dialog.closeAll();
}
}

View File

@@ -0,0 +1,182 @@
/**
* The app's icon set, inlined (same stroke style as the landing's).
*
* `@pi/ui` ships no SVGs: `pi-icon` renders a `mat-icon` whose name is looked
* up in Material's registry. Registering literals avoids one HTTP request per
* icon and works offline.
*/
import {
inject,
makeEnvironmentProviders,
provideAppInitializer,
type EnvironmentProviders,
} from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import { MatIconRegistry } from '@angular/material/icon';
export const APP_ICONS: Record<string, string> = {
'arrow-forward':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12h15M13 6l6 6-6 6"/></svg>',
briefcase:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="7" width="18" height="13" rx="2"/><path d="M9 7V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2M3 12h18"/></svg>',
buildings:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21V8l6-3v16M9 21h12V11l-6-2M13 21v-4h4v4"/><path d="M6 11h.01M6 15h.01"/></svg>',
calendar:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4"/></svg>',
chat: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20.5l1.4-5.2A8 8 0 1 1 21 12Z"/></svg>',
'check-rounded':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="m8 12.2 2.7 2.8L16 9.5"/></svg>',
check:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m4.5 12.5 5 5 10-11"/></svg>',
'chevron-back':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m14.5 6-6 6 6 6"/></svg>',
'chevron-down':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9.5 6 6 6-6"/></svg>',
'chevron-forward':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m9.5 6 6 6-6 6"/></svg>',
'chevron-up':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m6 14.5 6-6 6 6"/></svg>',
'close-rounded':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="m9 9 6 6M15 9l-6 6"/></svg>',
close:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6 6 18"/></svg>',
coins:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><ellipse cx="12" cy="6.5" rx="7" ry="3"/><path d="M5 6.5v5c0 1.7 3.1 3 7 3s7-1.3 7-3v-5M5 11.5v5c0 1.7 3.1 3 7 3s7-1.3 7-3v-5"/></svg>',
document:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z"/><path d="M14 3v5h5M9 13h6M9 17h4"/></svg>',
email:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3.5 7 7.4 5.3a2 2 0 0 0 2.2 0L20.5 7"/></svg>',
exclamation:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5.5M12 16.3h.01"/></svg>',
focus:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M3 8V5a2 2 0 0 1 2-2h3M16 3h3a2 2 0 0 1 2 2v3M21 16v3a2 2 0 0 1-2 2h-3M8 21H5a2 2 0 0 1-2-2v-3"/></svg>',
globe:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 0 1 0 18 15 15 0 0 1 0-18Z"/></svg>',
graphic:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20V4M4 20h16"/><path d="m7.5 15 3.5-4 3 2.6L20 7"/></svg>',
grid: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7.5" height="7.5" rx="1.5"/><rect x="13.5" y="3" width="7.5" height="7.5" rx="1.5"/><rect x="3" y="13.5" width="7.5" height="7.5" rx="1.5"/><rect x="13.5" y="13.5" width="7.5" height="7.5" rx="1.5"/></svg>',
info: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 11v5.5M12 7.7h.01"/></svg>',
'light-bulb':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6M10 21h4"/><path d="M12 3a6 6 0 0 0-3.5 10.9c.6.4 1 1.1 1 1.9v.2h5v-.2c0-.8.4-1.5 1-1.9A6 6 0 0 0 12 3Z"/></svg>',
link: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M10 13a5 5 0 0 0 7.1.1l2.9-2.9a5 5 0 0 0-7.1-7.1L11 4.9"/><path d="M14 11a5 5 0 0 0-7.1-.1L4 13.8a5 5 0 0 0 7.1 7.1l1.9-1.9"/></svg>',
list: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M8 6h13M8 12h13M8 18h13M3.5 6h.01M3.5 12h.01M3.5 18h.01"/></svg>',
location:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 21s7-5.6 7-11a7 7 0 1 0-14 0c0 5.4 7 11 7 11Z"/><circle cx="12" cy="10" r="2.5"/></svg>',
moon: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 13.4A8 8 0 1 1 10.6 4a6.6 6.6 0 0 0 9.4 9.4Z"/></svg>',
'play-arrow':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M8 5.5v13l10-6.5-10-6.5Z"/></svg>',
search:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="6.5"/><path d="m16 16 4.5 4.5"/></svg>',
'star-fill':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="m12 3.6 2.5 5.2 5.7.8-4.1 4 1 5.6-5.1-2.7-5.1 2.7 1-5.6-4.1-4 5.7-.8L12 3.6Z"/></svg>',
sun: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2M5.3 5.3l1.4 1.4M17.3 17.3l1.4 1.4M18.7 5.3l-1.4 1.4M6.7 17.3l-1.4 1.4"/></svg>',
support:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M9.5 9.5 5.6 5.6M14.5 9.5l3.9-3.9M14.5 14.5l3.9 3.9M9.5 14.5l-3.9 3.9"/><circle cx="12" cy="12" r="3.5"/></svg>',
'time-detail':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7v5.2l3.2 2"/></svg>',
user: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21v-1a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v1"/></svg>',
'verification-list':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 4h9a2 2 0 0 1 2 2v13a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V8"/><path d="M4 5.5 5.8 7.5 9 3.8M9 11h7M9 15h5"/></svg>',
plus: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>',
bug: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="7" y="7" width="10" height="13" rx="5"/><path d="M12 11v9M9 4l1.5 2.5M15 4l-1.5 2.5M3 13h4M17 13h4M4 8l3 2M20 8l-3 2M4 19l3-2M20 19l-3-2"/></svg>',
kanban:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M9 4v16M15 4v16"/></svg>',
rocket:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 15c-1 1-1.5 4-1.5 4.5s3.5-.5 4.5-1.5M9 12l3 3M14.5 4.5c2-1 4.5-1 5-.5s.5 3-.5 5L13 15.5 8.5 11Z"/><circle cx="15.5" cy="8.5" r="1.25"/></svg>',
users:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8" r="3.5"/><path d="M2.5 20a6.5 6.5 0 0 1 13 0M16 4.6a3.5 3.5 0 0 1 0 6.8M18.5 14.5a6.5 6.5 0 0 1 3 5.5"/></svg>',
logout:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M15 4h3a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-3M10 16l-4-4 4-4M6 12h10"/></svg>',
trash:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13M10 11v6M14 11v6"/></svg>',
edit: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20h4L19 9l-4-4L4 16v4ZM13.5 6.5l4 4"/></svg>',
activity:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12h4l3-8 4 16 3-8h4"/></svg>',
flag: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 21V4M5 4h11l-2 4 2 4H5"/></svg>',
filter:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 5h16l-6 8v6l-4-2v-4Z"/></svg>',
qa: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="7"/><path d="m8.5 11 2 2 3.5-4M20 20l-4-4"/></svg>',
figma:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M9 3h3v6H9a3 3 0 0 1 0-6ZM12 3h3a3 3 0 0 1 0 6h-3ZM9 9h3v6H9a3 3 0 0 1 0-6ZM12 12a3 3 0 1 1 6 0 3 3 0 0 1-6 0ZM9 15h3v3a3 3 0 1 1-3-3Z"/></svg>',
key: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="15" r="4"/><path d="m11 12 9-9M16 7l3 3M14 9l2 2"/></svg>',
archive:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="5" rx="1"/><path d="M5 9v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9M10 13h4"/></svg>',
feature:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3 2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.4 6.8 19.1l1-5.8L3.5 9.2l5.9-.9Z"/></svg>',
task: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="4" width="16" height="16" rx="3"/><path d="m8.5 12 2.5 2.5 4.5-5"/></svg>',
palette:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a9 9 0 0 0 0 18c1.1 0 1.5-.8 1.5-1.5 0-1.2-1-1.5-1-2.5s.7-1.5 1.7-1.5H17a4 4 0 0 0 4-4c0-4.7-4-8.5-9-8.5Z"/><circle cx="7.5" cy="11" r="1"/><circle cx="10" cy="7" r="1"/><circle cx="15" cy="7.5" r="1"/></svg>',
eye: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12Z"/><circle cx="12" cy="12" r="3"/></svg>',
'eye-off':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3l18 18M10.6 5.1A10.4 10.4 0 0 1 12 5c6.5 0 10 7 10 7a17 17 0 0 1-3.2 4.1M6.6 6.6A17 17 0 0 0 2 12s3.5 7 10 7a10 10 0 0 0 5.4-1.6M9.9 9.9a3 3 0 0 0 4.2 4.2"/></svg>',
add: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 5v14M5 12h14"/></svg>',
substract:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/></svg>',
'arrow-back':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M20 12H5M11 6l-6 6 6 6"/></svg>',
'arrow-up':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20V5M6 11l6-6 6 6"/></svg>',
'arrow-down':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v15M6 13l6 6 6-6"/></svg>',
'arrow-up-rounded':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 16V8M8.5 11.5 12 8l3.5 3.5"/></svg>',
'arrow-secondary-back':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m14.5 6-6 6 6 6"/></svg>',
'arrow-secondary-forward':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m9.5 6 6 6-6 6"/></svg>',
'arrow-secondary-up':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m6 14.5 6-6 6 6"/></svg>',
'arrow-secondary-down':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9.5 6 6 6-6"/></svg>',
'double-chevron-back':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m12 6-6 6 6 6M18 6l-6 6 6 6"/></svg>',
error:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5.5M12 16.5h.01"/></svg>',
'info-rounded':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M12 11v5.5M12 7.5h.01"/></svg>',
lock: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg>',
more: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="5" cy="12" r="1.25"/><circle cx="12" cy="12" r="1.25"/><circle cx="19" cy="12" r="1.25"/></svg>',
'open-details':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5"/></svg>',
'star-outline':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3 2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.4 6.8 19.1l1-5.8L3.5 9.2l5.9-.9Z"/></svg>',
'star-primary':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="m12 3 2.6 5.3 5.9.9-4.3 4.1 1 5.8L12 16.4 6.8 19.1l1-5.8L3.5 9.2l5.9-.9Z"/></svg>',
start:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M7 5v14l11-7Z"/></svg>',
upload:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 16V4M7 9l5-5 5 5M4 20h16"/></svg>',
download:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v12M7 11l5 5 5-5M4 20h16"/></svg>',
'download-teaser':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 4v12M7 11l5 5 5-5M4 20h16"/></svg>',
'calendar-plus':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M3 10h18M8 3v4M16 3v4M12 13v5M9.5 15.5h5"/></svg>',
bullet:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="2.5"/></svg>',
'time-zone':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18"/></svg>',
'user-profile':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>',
'user-profile-advisor':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M4 21a8 8 0 0 1 16 0"/></svg>',
'user-profile-company':
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3 21V8l6-3v16M9 21h12V11l-6-2"/></svg>',
whatsapp:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a8 8 0 0 1-11.6 7.1L4 20.5l1.4-5.2A8 8 0 1 1 21 12Z"/></svg>',
};
export function provideAppIcons(): EnvironmentProviders {
return makeEnvironmentProviders([
provideAppInitializer(() => {
const registry = inject(MatIconRegistry);
const sanitizer = inject(DomSanitizer);
for (const [name, svg] of Object.entries(APP_ICONS)) {
registry.addSvgIconLiteral(
name,
sanitizer.bypassSecurityTrustHtml(svg),
);
}
}),
]);
}

View File

@@ -0,0 +1,30 @@
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
/** Pireon mark (same asset as pireon.dev, served from `apps/web/public`). */
@Component({
selector: 'ts-logo',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<img
src="logo-96.png"
srcset="logo-96.png 96w, logo.png 256w"
[attr.sizes]="size() + 'px'"
alt=""
[width]="size()"
[height]="size()"
decoding="async"
/>
`,
styles: `
:host {
display: inline-flex;
}
img {
display: block;
object-fit: contain;
}
`,
})
export class LogoComponent {
readonly size = input(24);
}

View File

@@ -0,0 +1,40 @@
/**
* The few strings `@pi/ui` translates itself (field errors, close labels,
* chart legends), provided synchronously instead of `providePiTranslate()`
* (which fetches ./assets/i18n/*.json).
*/
import {
inject,
makeEnvironmentProviders,
provideAppInitializer,
type EnvironmentProviders,
} from '@angular/core';
import { provideTranslateService, TranslateService } from '@ngx-translate/core';
export function providePiStrings(): EnvironmentProviders {
return makeEnvironmentProviders([
provideTranslateService({ fallbackLang: 'es' }),
provideAppInitializer(() => {
const translate = inject(TranslateService);
translate.setTranslation('es', {
common: {
close: 'Cerrar',
edit: 'Editar',
legend: 'Leyenda',
min: 'Mín',
max: 'Máx',
confidentialInfo: 'Información confidencial',
},
// Signal Forms error kinds (pi fields show `error.message ?? error.kind`).
required: 'Campo obligatorio',
email: 'Email no válido',
minLength: 'Demasiado corto',
maxLength: 'Demasiado largo',
pattern: 'Formato no válido',
min: 'Valor demasiado bajo',
max: 'Valor demasiado alto',
});
translate.use('es');
}),
]);
}

View File

@@ -0,0 +1,25 @@
import {
inject,
makeEnvironmentProviders,
provideAppInitializer,
type EnvironmentProviders,
} from '@angular/core';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import { PiThemeService } from '@pi/ui';
import { AppearanceService } from './appearance';
import { provideAppIcons } from './icons';
import { providePiStrings } from './pi-strings';
/** Everything `@pi/ui` needs: animations, icons, strings, theme and dark mode. */
export function providePiUi(): EnvironmentProviders {
return makeEnvironmentProviders([
// @pi/ui still uses @angular/animations (collapse/fade triggers), so this stays
// despite its deprecation in Angular 20.2.
provideAnimationsAsync(),
provideAppIcons(),
providePiStrings(),
// `default` is the library's application shell (the landing uses `alternative`, its marketing one).
provideAppInitializer(() => inject(PiThemeService).setTheme('default')),
provideAppInitializer(() => inject(AppearanceService).restore()),
]);
}

View File

@@ -0,0 +1,37 @@
import { inject, Injectable } from '@angular/core';
import { MatSnackBar } from '@angular/material/snack-bar';
import { PiSnackBarComponent } from '@pi/ui';
type ToastType = 'success' | 'error' | 'info';
/** Feedback toasts with the library's `pi-snack-bar`. */
@Injectable({ providedIn: 'root' })
export class ToastService {
private readonly snackBar = inject(MatSnackBar);
success(message: string): void {
this.open(message, 'success', 'check-rounded', 3000);
}
error(message: string): void {
this.open(message, 'error', 'exclamation', 6000);
}
info(message: string): void {
this.open(message, 'info', 'info', 3000);
}
private open(
message: string,
type: ToastType,
icon: string,
duration: number,
): void {
this.snackBar.openFromComponent(PiSnackBarComponent, {
data: { message, type, icon, dismissible: true },
duration,
horizontalPosition: 'end',
verticalPosition: 'bottom',
});
}
}

View File

@@ -0,0 +1,27 @@
{
"extends": "../../../../tsconfig.base.json",
"compilerOptions": {
"isolatedModules": true,
"target": "es2022",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"emitDecoratorMetadata": false,
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}

View File

@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"include": ["src/**/*.ts"],
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
}