first commit
This commit is contained in:
20
libs/web/shared/infrastructure/README.md
Normal file
20
libs/web/shared/infrastructure/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# web-shared-infrastructure
|
||||
|
||||
Cliente GraphQL de la web.
|
||||
|
||||
- **Import**: `@task-stream/web/shared/infrastructure`
|
||||
- **Capa**: `infrastructure` · **Tags**: `scope:web-shared`, `type:infrastructure`
|
||||
|
||||
## Contenido
|
||||
|
||||
- `provideGraphql()`: Apollo con HTTP para queries/mutations y `graphql-ws` para subscriptions; caché desactivada.
|
||||
- `AccessTokenSource` (puerto): de dónde sale el token (lo implementa identity).
|
||||
- `API_CONFIG` / `apiConfigFromLocation()`: la API se sirve en el mismo origen (`/api`, `/graphql`).
|
||||
|
||||
## Comandos
|
||||
|
||||
```sh
|
||||
npx nx lint web-shared-infrastructure
|
||||
```
|
||||
|
||||
Reglas de dependencia entre capas: [Arquitectura](../../../../docs/02-arquitectura.md#21-capas-hexagonales).
|
||||
34
libs/web/shared/infrastructure/eslint.config.mjs
Normal file
34
libs/web/shared/infrastructure/eslint.config.mjs
Normal 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: {},
|
||||
},
|
||||
];
|
||||
13
libs/web/shared/infrastructure/project.json
Normal file
13
libs/web/shared/infrastructure/project.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "web-shared-infrastructure",
|
||||
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/web/shared/infrastructure/src",
|
||||
"prefix": "ts",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:web-shared", "type:infrastructure"],
|
||||
"targets": {
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
4
libs/web/shared/infrastructure/src/index.ts
Normal file
4
libs/web/shared/infrastructure/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './lib/api-config';
|
||||
export * from './lib/access-token-source';
|
||||
export * from './lib/graphql.provider';
|
||||
export * from './lib/graphql-errors';
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Port used by the GraphQL client to authenticate requests. Implemented by
|
||||
* the identity context (which knows how to refresh and sign out).
|
||||
*/
|
||||
export abstract class AccessTokenSource {
|
||||
/** A valid access token (refreshed if needed), or null when signed out. */
|
||||
abstract accessToken(): Promise<string | null>;
|
||||
/** The API rejected the token: the session is over. */
|
||||
abstract unauthenticated(): void;
|
||||
}
|
||||
32
libs/web/shared/infrastructure/src/lib/api-config.ts
Normal file
32
libs/web/shared/infrastructure/src/lib/api-config.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
InjectionToken,
|
||||
makeEnvironmentProviders,
|
||||
type EnvironmentProviders,
|
||||
} from '@angular/core';
|
||||
|
||||
export interface ApiConfig {
|
||||
/** Base of the REST endpoints, e.g. `/api`. */
|
||||
restUrl: string;
|
||||
/** GraphQL over HTTP, e.g. `/graphql`. */
|
||||
graphqlUrl: string;
|
||||
/** GraphQL subscriptions (graphql-ws), e.g. `ws://localhost:4200/graphql`. */
|
||||
graphqlWsUrl: string;
|
||||
}
|
||||
|
||||
export const API_CONFIG = new InjectionToken<ApiConfig>('API_CONFIG');
|
||||
|
||||
export function provideApiConfig(config: ApiConfig): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([{ provide: API_CONFIG, useValue: config }]);
|
||||
}
|
||||
|
||||
/** Relative URLs resolved against the page, so the dev proxy / reverse proxy can serve the API. */
|
||||
export function apiConfigFromLocation(
|
||||
location: Location = window.location,
|
||||
): ApiConfig {
|
||||
const ws = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
return {
|
||||
restUrl: '/api',
|
||||
graphqlUrl: '/graphql',
|
||||
graphqlWsUrl: `${ws}//${location.host}/graphql`,
|
||||
};
|
||||
}
|
||||
10
libs/web/shared/infrastructure/src/lib/graphql-errors.ts
Normal file
10
libs/web/shared/infrastructure/src/lib/graphql-errors.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
|
||||
/** A message fit for a toast from whatever the GraphQL client threw. */
|
||||
export function errorMessage(error: unknown): string {
|
||||
if (CombinedGraphQLErrors.is(error)) {
|
||||
return error.errors.map((e) => e.message).join('\n');
|
||||
}
|
||||
if (error instanceof Error) return error.message;
|
||||
return String(error);
|
||||
}
|
||||
95
libs/web/shared/infrastructure/src/lib/graphql.provider.ts
Normal file
95
libs/web/shared/infrastructure/src/lib/graphql.provider.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
inject,
|
||||
makeEnvironmentProviders,
|
||||
type EnvironmentProviders,
|
||||
} from '@angular/core';
|
||||
import { ApolloLink, InMemoryCache } from '@apollo/client';
|
||||
import { SetContextLink } from '@apollo/client/link/context';
|
||||
import { ErrorLink } from '@apollo/client/link/error';
|
||||
import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
|
||||
import { getMainDefinition } from '@apollo/client/utilities';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import { provideApollo } from 'apollo-angular';
|
||||
import { HttpLink } from 'apollo-angular/http';
|
||||
import { createClient } from 'graphql-ws';
|
||||
import { AccessTokenSource } from './access-token-source';
|
||||
import { API_CONFIG } from './api-config';
|
||||
|
||||
/**
|
||||
* Apollo client for the task-stream API: HTTP for queries/mutations,
|
||||
* graphql-ws for subscriptions (token in `connection_init`).
|
||||
*
|
||||
* The cache is disabled on purpose: state lives in the NgRx signal stores,
|
||||
* which are fed by queries and kept fresh by subscriptions.
|
||||
*/
|
||||
export function provideGraphql(): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([
|
||||
provideApollo(() => {
|
||||
const config = inject(API_CONFIG);
|
||||
const tokens = inject(AccessTokenSource);
|
||||
const httpLink = inject(HttpLink);
|
||||
|
||||
const auth = new SetContextLink(async (prevContext) => {
|
||||
const token = await tokens.accessToken();
|
||||
return token
|
||||
? {
|
||||
headers: {
|
||||
...prevContext['headers'],
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
: {};
|
||||
});
|
||||
|
||||
const errors = new ErrorLink(({ error }) => {
|
||||
if (
|
||||
CombinedGraphQLErrors.is(error) &&
|
||||
error.errors.some((e) => e.extensions?.['code'] === 'UNAUTHENTICATED')
|
||||
) {
|
||||
tokens.unauthenticated();
|
||||
}
|
||||
});
|
||||
|
||||
const ws = new GraphQLWsLink(
|
||||
createClient({
|
||||
url: config.graphqlWsUrl,
|
||||
lazy: true,
|
||||
retryAttempts: Infinity,
|
||||
shouldRetry: () => true,
|
||||
connectionParams: async () => {
|
||||
const token = await tokens.accessToken();
|
||||
return token ? { authorization: `Bearer ${token}` } : {};
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const http = ApolloLink.from([
|
||||
auth,
|
||||
errors,
|
||||
httpLink.create({ uri: config.graphqlUrl }),
|
||||
]);
|
||||
|
||||
const link = ApolloLink.split(
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query);
|
||||
return (
|
||||
definition.kind === 'OperationDefinition' &&
|
||||
definition.operation === 'subscription'
|
||||
);
|
||||
},
|
||||
ApolloLink.from([errors, ws]),
|
||||
http,
|
||||
);
|
||||
|
||||
return {
|
||||
link,
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
query: { fetchPolicy: 'no-cache' },
|
||||
watchQuery: { fetchPolicy: 'no-cache' },
|
||||
mutate: { fetchPolicy: 'no-cache' },
|
||||
},
|
||||
};
|
||||
}),
|
||||
]);
|
||||
}
|
||||
27
libs/web/shared/infrastructure/tsconfig.json
Normal file
27
libs/web/shared/infrastructure/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
libs/web/shared/infrastructure/tsconfig.lib.json
Normal file
12
libs/web/shared/infrastructure/tsconfig.lib.json
Normal 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"]
|
||||
}
|
||||
24
libs/web/shared/ui/README.md
Normal file
24
libs/web/shared/ui/README.md
Normal 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).
|
||||
34
libs/web/shared/ui/eslint.config.mjs
Normal file
34
libs/web/shared/ui/eslint.config.mjs
Normal 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: {},
|
||||
},
|
||||
];
|
||||
13
libs/web/shared/ui/project.json
Normal file
13
libs/web/shared/ui/project.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
7
libs/web/shared/ui/src/index.ts
Normal file
7
libs/web/shared/ui/src/index.ts
Normal 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';
|
||||
188
libs/web/shared/ui/src/lib/app-shell.scss
Normal file
188
libs/web/shared/ui/src/lib/app-shell.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
54
libs/web/shared/ui/src/lib/app-shell.ts
Normal file
54
libs/web/shared/ui/src/lib/app-shell.ts
Normal 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);
|
||||
}
|
||||
46
libs/web/shared/ui/src/lib/appearance.ts
Normal file
46
libs/web/shared/ui/src/lib/appearance.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
24
libs/web/shared/ui/src/lib/drawer.service.ts
Normal file
24
libs/web/shared/ui/src/lib/drawer.service.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
182
libs/web/shared/ui/src/lib/icons.ts
Normal file
182
libs/web/shared/ui/src/lib/icons.ts
Normal 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),
|
||||
);
|
||||
}
|
||||
}),
|
||||
]);
|
||||
}
|
||||
30
libs/web/shared/ui/src/lib/logo.ts
Normal file
30
libs/web/shared/ui/src/lib/logo.ts
Normal 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);
|
||||
}
|
||||
40
libs/web/shared/ui/src/lib/pi-strings.ts
Normal file
40
libs/web/shared/ui/src/lib/pi-strings.ts
Normal 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');
|
||||
}),
|
||||
]);
|
||||
}
|
||||
25
libs/web/shared/ui/src/lib/pi-ui.providers.ts
Normal file
25
libs/web/shared/ui/src/lib/pi-ui.providers.ts
Normal 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()),
|
||||
]);
|
||||
}
|
||||
37
libs/web/shared/ui/src/lib/toast.service.ts
Normal file
37
libs/web/shared/ui/src/lib/toast.service.ts
Normal 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',
|
||||
});
|
||||
}
|
||||
}
|
||||
27
libs/web/shared/ui/tsconfig.json
Normal file
27
libs/web/shared/ui/tsconfig.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
libs/web/shared/ui/tsconfig.lib.json
Normal file
12
libs/web/shared/ui/tsconfig.lib.json
Normal 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"]
|
||||
}
|
||||
Reference in New Issue
Block a user