first commit
This commit is contained in:
47
apps/web/src/app/app.config.ts
Normal file
47
apps/web/src/app/app.config.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { provideHttpClient, withFetch } from '@angular/common/http';
|
||||
import {
|
||||
ApplicationConfig,
|
||||
provideBrowserGlobalErrorListeners,
|
||||
} from '@angular/core';
|
||||
import {
|
||||
provideRouter,
|
||||
withComponentInputBinding,
|
||||
withRouterConfig,
|
||||
} from '@angular/router';
|
||||
import { provideIdentityInfrastructure } from '@task-stream/web/identity/infrastructure';
|
||||
import {
|
||||
apiConfigFromLocation,
|
||||
provideApiConfig,
|
||||
provideGraphql,
|
||||
} from '@task-stream/web/shared/infrastructure';
|
||||
import { providePiUi, ToastService } from '@task-stream/web/shared/ui';
|
||||
import { Notifications } from '@task-stream/web/tracker/application';
|
||||
import { provideTrackerInfrastructure } from '@task-stream/web/tracker/infrastructure';
|
||||
import { appRoutes } from './app.routes';
|
||||
|
||||
/*
|
||||
* Composition root of the web client:
|
||||
* presenters (routes/pages) → application (NgRx signal stores, events) → domain ports ← infrastructure (GraphQL/HTTP)
|
||||
*/
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideRouter(
|
||||
appRoutes,
|
||||
// Route params / query params straight into component inputs (`key`, `issueKey`, `returnUrl`…).
|
||||
withComponentInputBinding(),
|
||||
withRouterConfig({ paramsInheritanceStrategy: 'always' }),
|
||||
),
|
||||
provideHttpClient(withFetch()),
|
||||
providePiUi(),
|
||||
|
||||
// API: same origin, proxied in development (proxy.conf.json) and by nginx in Docker.
|
||||
provideApiConfig(apiConfigFromLocation()),
|
||||
provideGraphql(),
|
||||
|
||||
// Ports → adapters
|
||||
provideIdentityInfrastructure(),
|
||||
provideTrackerInfrastructure(),
|
||||
{ provide: Notifications, useExisting: ToastService },
|
||||
],
|
||||
};
|
||||
26
apps/web/src/app/app.routes.spec.ts
Normal file
26
apps/web/src/app/app.routes.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { authGuard } from '@task-stream/web/identity/presenters';
|
||||
import { appRoutes } from './app.routes';
|
||||
|
||||
describe('appRoutes', () => {
|
||||
it('keeps sign-in public and everything else behind the auth guard', () => {
|
||||
const paths = appRoutes.map((route) => route.path);
|
||||
expect(paths).toEqual(expect.arrayContaining(['login', 'auth/callback', '', '**']));
|
||||
|
||||
const app = appRoutes.find((route) => route.path === '' && route.children);
|
||||
expect(app?.canActivate).toContain(authGuard);
|
||||
expect(app?.children?.map((route) => route.path)).toEqual(['', 'p/:key']);
|
||||
});
|
||||
|
||||
it('exposes every project section', () => {
|
||||
const project = appRoutes.find((route) => route.path === '')?.children?.find((route) => route.path === 'p/:key');
|
||||
expect(project?.children?.map((route) => route.path)).toEqual([
|
||||
'',
|
||||
'board',
|
||||
'issues/:issueKey',
|
||||
'releases',
|
||||
'dashboard',
|
||||
'activity',
|
||||
'members',
|
||||
]);
|
||||
});
|
||||
});
|
||||
12
apps/web/src/app/app.routes.ts
Normal file
12
apps/web/src/app/app.routes.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Route } from '@angular/router';
|
||||
import {
|
||||
authGuard,
|
||||
identityRoutes,
|
||||
} from '@task-stream/web/identity/presenters';
|
||||
import { trackerRoutes } from '@task-stream/web/tracker/presenters';
|
||||
|
||||
export const appRoutes: Route[] = [
|
||||
...identityRoutes,
|
||||
{ path: '', canActivate: [authGuard], children: trackerRoutes },
|
||||
{ path: '**', redirectTo: '' },
|
||||
];
|
||||
39
apps/web/src/app/app.ts
Normal file
39
apps/web/src/app/app.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router';
|
||||
import { AuthFacade } from '@task-stream/web/identity/application';
|
||||
import { UserMenuComponent } from '@task-stream/web/identity/presenters';
|
||||
import { AppShellComponent } from '@task-stream/web/shared/ui';
|
||||
|
||||
@Component({
|
||||
selector: 'ts-root',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
RouterOutlet,
|
||||
RouterLink,
|
||||
RouterLinkActive,
|
||||
AppShellComponent,
|
||||
UserMenuComponent,
|
||||
],
|
||||
template: `
|
||||
<ts-app-shell>
|
||||
@if (auth.isAuthenticated()) {
|
||||
<a
|
||||
shellNav
|
||||
class="ts-nav-link"
|
||||
routerLink="/"
|
||||
routerLinkActive="is-active"
|
||||
[routerLinkActiveOptions]="{ exact: true }"
|
||||
>
|
||||
Proyectos
|
||||
</a>
|
||||
}
|
||||
@if (auth.isAuthenticated()) {
|
||||
<ts-user-menu shellUser />
|
||||
}
|
||||
<router-outlet />
|
||||
</ts-app-shell>
|
||||
`,
|
||||
})
|
||||
export class App {
|
||||
protected readonly auth = inject(AuthFacade);
|
||||
}
|
||||
39
apps/web/src/index.html
Normal file
39
apps/web/src/index.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>TaskStream</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Gestión de proyectos y tickets en tiempo real"
|
||||
/>
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="logo-96.png" />
|
||||
<link rel="apple-touch-icon" href="logo.png" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<!-- Inter, like pireon.dev · Geist Mono only for data (ticket keys, counters, versions) -->
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400..800&family=Geist+Mono:wght@400..600&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<meta name="theme-color" content="#f7f7fa" />
|
||||
<script>
|
||||
// Dark mode before first paint (same key as AppearanceService).
|
||||
(function () {
|
||||
try {
|
||||
var stored = localStorage.getItem('task-stream.appearance');
|
||||
var dark = stored
|
||||
? stored === 'dark'
|
||||
: window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
if (dark) document.documentElement.classList.add('dark-mode');
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body class="pi-shell pi-shell__default">
|
||||
<ts-root></ts-root>
|
||||
</body>
|
||||
</html>
|
||||
5
apps/web/src/main.ts
Normal file
5
apps/web/src/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
|
||||
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
|
||||
551
apps/web/src/styles.scss
Normal file
551
apps/web/src/styles.scss
Normal file
@@ -0,0 +1,551 @@
|
||||
// Global stylesheet — same approach as landing-web.
|
||||
//
|
||||
// `@pi/ui/styles/common/index.scss` is deliberately NOT used: its tail calls
|
||||
// `mat.define-palette()` / `mat.define-light-theme()`, removed from Angular
|
||||
// Material in v19. Its parts compile, so they are pulled one by one and the
|
||||
// Material theme is declared here with the current API.
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
@use '@pi/ui/styles/common/abstracts' as *;
|
||||
@use '@pi/ui/styles/common/base' as *;
|
||||
@use '@pi/ui/styles/common/components' as *;
|
||||
@use '@pi/ui/styles/common/layouts' as *;
|
||||
@use '@pi/ui/styles/common/vendors' as *;
|
||||
@use '@pi/ui/styles/common/wrappers' as *;
|
||||
@use '@pi/ui/styles/themes' as *;
|
||||
|
||||
// One ramp from a single hue against a surface (the library's components pick
|
||||
// from anywhere along 10 → 900, so a brand override must cover all of it).
|
||||
@mixin brand-ramp($name, $hue, $surface) {
|
||||
--pi-color-#{$name}-10: #{color-mix(in srgb, $hue 5%, $surface)};
|
||||
--pi-color-#{$name}-25: #{color-mix(in srgb, $hue 9%, $surface)};
|
||||
--pi-color-#{$name}-50: #{color-mix(in srgb, $hue 13%, $surface)};
|
||||
--pi-color-#{$name}-75: #{color-mix(in srgb, $hue 17%, $surface)};
|
||||
--pi-color-#{$name}-100: #{color-mix(in srgb, $hue 23%, $surface)};
|
||||
--pi-color-#{$name}-200: #{color-mix(in srgb, $hue 55%, $surface)};
|
||||
--pi-color-#{$name}-300: #{color-mix(in srgb, $hue 75%, $surface)};
|
||||
--pi-color-#{$name}-400: #{$hue};
|
||||
--pi-color-#{$name}: #{$hue};
|
||||
--pi-color-#{$name}-900: #{color-mix(in srgb, $hue 72%, black)};
|
||||
--pi-color-#{$name}-200-rgba: #{rgba($hue, 0.18)};
|
||||
}
|
||||
|
||||
@mixin dark-ramp($name, $hue) {
|
||||
--pi-color-#{$name}-10: #{color-mix(in srgb, $hue 5%, #161922)};
|
||||
--pi-color-#{$name}-25: #{color-mix(in srgb, $hue 9%, #161922)};
|
||||
--pi-color-#{$name}-50: #{color-mix(in srgb, $hue 13%, #161922)};
|
||||
--pi-color-#{$name}-75: #{color-mix(in srgb, $hue 17%, #161922)};
|
||||
--pi-color-#{$name}-100: #{color-mix(in srgb, $hue 23%, #161922)};
|
||||
--pi-color-#{$name}-200: #{color-mix(in srgb, $hue 55%, #161922)};
|
||||
--pi-color-#{$name}-300: #{color-mix(in srgb, $hue 75%, #161922)};
|
||||
--pi-color-#{$name}-400: #{$hue};
|
||||
--pi-color-#{$name}: #{$hue};
|
||||
--pi-color-#{$name}-900: #{color-mix(in srgb, $hue 70%, #ffffff)};
|
||||
--pi-color-#{$name}-200-rgba: #{rgba($hue, 0.18)};
|
||||
}
|
||||
|
||||
html {
|
||||
@include mat.theme(
|
||||
(
|
||||
color: (
|
||||
primary: mat.$violet-palette,
|
||||
tertiary: mat.$cyan-palette,
|
||||
),
|
||||
typography: Inter,
|
||||
density: -1,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// `pi-action-button size="w100"`: the library resets the button with `all: unset`
|
||||
// (content-box) and adds horizontal padding, so 100% + padding overflowed its container.
|
||||
// Three classes to beat the component's own `[_nghost] > button[_ngcontent]` rule.
|
||||
.pi-button.pi-button__action.pi-button__w100 {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Design tokens — the Pireon platform look (same palette, type and surfaces as
|
||||
// pireon.dev, see landing-web), plus one colour per status and per ticket type,
|
||||
// which carry meaning everywhere (board, cards, charts, history).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.pi-shell {
|
||||
// Pireon brand: the logo's two hues (276° violet, 205° blue), darkened to clear 4.5:1.
|
||||
@include brand-ramp('primary', #a347e1, #ffffff);
|
||||
@include brand-ramp('accent', #1879bf, #ffffff);
|
||||
@include brand-ramp('highlighted', #6c2fb5, #ffffff);
|
||||
|
||||
--ts-font-display: Inter, system-ui, -apple-system, sans-serif;
|
||||
--ts-font-body: Inter, system-ui, -apple-system, sans-serif;
|
||||
--ts-font-mono:
|
||||
'Geist Mono', ui-monospace, 'SFMono-Regular', Menlo, monospace;
|
||||
|
||||
--ts-bg: #f7f7fa;
|
||||
--ts-surface: #ffffff;
|
||||
--ts-surface-raised: #ffffff;
|
||||
--ts-sunken: #f0f0f5;
|
||||
--ts-line: #e4e4ea;
|
||||
--ts-line-strong: #d3d3dc;
|
||||
--ts-ink: #17171f;
|
||||
--ts-ink-soft: #44445a;
|
||||
--ts-muted: #5b5b6b;
|
||||
--ts-brand: var(--pi-color-primary);
|
||||
--ts-accent: var(--pi-color-accent);
|
||||
--ts-radius: var(--pi-theme-card-radius, 1.5rem);
|
||||
--ts-shadow: 0 1px 2px rgb(23 23 31 / 4%);
|
||||
--ts-lift: 0 18px 40px -18px
|
||||
color-mix(in srgb, var(--pi-color-primary) 45%, transparent);
|
||||
|
||||
// Workflow
|
||||
--ts-s-backlog: #8b8b9a;
|
||||
--ts-s-todo: #64748b;
|
||||
--ts-s-progress: #a347e1;
|
||||
--ts-s-review: #1879bf;
|
||||
--ts-s-ready: #b7791f;
|
||||
--ts-s-qa: #ea580c;
|
||||
--ts-s-failed: #dc2626;
|
||||
--ts-s-done: #15803d;
|
||||
--ts-s-cancelled: #a8a8b3;
|
||||
|
||||
// Ticket types
|
||||
--ts-t-bug: #e5484d;
|
||||
--ts-t-feature: #a347e1;
|
||||
--ts-t-ui: #d6409f;
|
||||
--ts-t-task: #64748b;
|
||||
--ts-t-qa: #d97706;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: var(--ts-font-body);
|
||||
background-color: var(--ts-bg);
|
||||
color: var(--ts-ink);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--ts-brand);
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-family: var(--ts-font-display);
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--ts-ink);
|
||||
}
|
||||
|
||||
/* The accent words of a title, in the logo's gradient (as on pireon.dev). */
|
||||
.ts-gradient-text {
|
||||
background: linear-gradient(
|
||||
100deg,
|
||||
var(--pi-color-primary),
|
||||
var(--pi-color-accent)
|
||||
);
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: color-mix(in srgb, var(--ts-brand) 25%, transparent);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// App primitives shared by every page.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
.ts-muted {
|
||||
color: var(--ts-muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Data: keys, counters, dates, versions. */
|
||||
.ts-mono {
|
||||
font-family: var(--ts-font-mono);
|
||||
font-feature-settings: 'zero';
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* Small uppercase line above a title — pireon.dev's `landing-eyebrow`. */
|
||||
.ts-eyebrow {
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.2em;
|
||||
text-transform: uppercase;
|
||||
color: var(--ts-accent);
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Ticket / project key: WEB-12. */
|
||||
.ts-key {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.0625rem 0.375rem;
|
||||
border-radius: 0.3125rem;
|
||||
font-family: var(--ts-font-mono);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0;
|
||||
color: var(--ts-ink-soft);
|
||||
background: var(--ts-sunken);
|
||||
border: 1px solid var(--ts-line);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Status / type pill coloured by --tone (set inline from copy.ts). */
|
||||
.ts-pill {
|
||||
--tone: var(--ts-muted);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding: 0.1875rem 0.5rem 0.1875rem 0.4375rem;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
color: color-mix(in srgb, var(--tone) 82%, var(--ts-ink));
|
||||
background: color-mix(in srgb, var(--tone) 12%, var(--ts-surface));
|
||||
border: 1px solid color-mix(in srgb, var(--tone) 28%, transparent);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
width: 0.4375rem;
|
||||
height: 0.4375rem;
|
||||
border-radius: 50%;
|
||||
background: var(--tone);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Round icon-only button drawn by the app. `@pi/ui` styles every <button>
|
||||
* globally (min-width 5.5rem, side padding, radius, no border) with selectors
|
||||
* too specific to override otherwise, hence the !important.
|
||||
*/
|
||||
.ts-icon-button {
|
||||
display: inline-grid !important;
|
||||
place-items: center;
|
||||
width: var(--ts-icon-button-size, 2rem);
|
||||
height: var(--ts-icon-button-size, 2rem);
|
||||
min-width: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: 1px solid var(--ts-icon-button-border, transparent) !important;
|
||||
border-radius: 999px !important;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--ts-brand);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Pireon's card (pireon.dev's `landing-card`): a faint violet→blue wash and a
|
||||
* gradient edge drawn with mask-composite. `.ts-panel` is the static version,
|
||||
* `.ts-card-link` adds the hover lift for cards that navigate.
|
||||
*/
|
||||
.ts-panel,
|
||||
.ts-card-link {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
border-radius: var(--ts-radius);
|
||||
background-image: linear-gradient(
|
||||
155deg,
|
||||
color-mix(in srgb, var(--pi-color-primary) 7%, var(--ts-surface)) 0%,
|
||||
var(--ts-surface) 45%,
|
||||
color-mix(in srgb, var(--pi-color-accent) 5%, var(--ts-surface)) 100%
|
||||
);
|
||||
box-shadow: var(--ts-shadow);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
border-radius: inherit;
|
||||
padding: 1px;
|
||||
background: linear-gradient(
|
||||
150deg,
|
||||
color-mix(in srgb, var(--pi-color-primary) 55%, transparent) 0%,
|
||||
color-mix(in srgb, var(--pi-color-accent) 35%, transparent) 55%,
|
||||
var(--ts-line) 100%
|
||||
);
|
||||
-webkit-mask:
|
||||
linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ts-card-link {
|
||||
display: block;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
transform 0.22s ease-out,
|
||||
box-shadow 0.22s ease-out;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: var(--ts-lift);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--ts-brand);
|
||||
outline-offset: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.ts-prewrap {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.ts-center {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
min-height: 40vh;
|
||||
}
|
||||
|
||||
.ts-plain-link {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.ts-page-header,
|
||||
.ts-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.75rem, 1.2rem + 1.6vw, 2.5rem);
|
||||
font-weight: 750;
|
||||
line-height: 1.05;
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.375rem 0 0;
|
||||
color: var(--ts-muted);
|
||||
}
|
||||
}
|
||||
|
||||
.ts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(18rem, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.ts-form {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.ts-form__row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
// Project tabs: a ruler with a thick ink marker under the current section.
|
||||
.ts-tabs {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin: 1.25rem 0 1.75rem;
|
||||
border-bottom: 1px solid var(--ts-line-strong);
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.ts-tabs a {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.625rem 0.875rem 0.75rem;
|
||||
color: var(--ts-muted);
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: color 0.15s ease;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: auto 0.5rem -1px;
|
||||
height: 3px;
|
||||
border-radius: 3px 3px 0 0;
|
||||
background: transparent;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--ts-ink);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--ts-brand);
|
||||
font-weight: 600;
|
||||
|
||||
&::after {
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--pi-color-primary),
|
||||
var(--pi-color-accent)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Header links, as in pireon.dev's header.
|
||||
.ts-nav-link {
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
color: var(--ts-muted);
|
||||
font-size: 0.9375rem;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
color: var(--ts-ink);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--ts-brand);
|
||||
font-weight: 600;
|
||||
background: var(--pi-color-primary-50);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
// `@pi/ui`'s reset gives every `li + li` a top margin, meant for prose.
|
||||
ul > li,
|
||||
ol > li {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dark mode. `@pi/ui` has no dark styles: `PiThemeService` toggles
|
||||
// `html.dark-mode` and the library's colour contract is redefined under it
|
||||
// (on `.pi-shell`, the <body>, where the theme writes its own ramps).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
html.dark-mode {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html.dark-mode .pi-shell {
|
||||
// Same dark palette as pireon.dev.
|
||||
--ts-bg: #0f1116;
|
||||
--ts-surface: #161922;
|
||||
--ts-surface-raised: #1c202b;
|
||||
--ts-sunken: #12141a;
|
||||
--ts-line: #262c3a;
|
||||
--ts-line-strong: #333a4b;
|
||||
--ts-ink: #f4f5f9;
|
||||
--ts-ink-soft: #d8dbe6;
|
||||
--ts-muted: #a9b0c1;
|
||||
--ts-shadow: 0 1px 2px rgb(0 0 0 / 30%);
|
||||
|
||||
--ts-s-backlog: #8a91a4;
|
||||
--ts-s-todo: #94a3b8;
|
||||
--ts-s-progress: #ae5de5;
|
||||
--ts-s-review: #339be6;
|
||||
--ts-s-ready: #eab308;
|
||||
--ts-s-qa: #fb923c;
|
||||
--ts-s-failed: #f07070;
|
||||
--ts-s-done: #34c98a;
|
||||
--ts-s-cancelled: #6b7180;
|
||||
|
||||
--ts-t-bug: #f07070;
|
||||
--ts-t-feature: #ae5de5;
|
||||
--ts-t-ui: #f472b6;
|
||||
--ts-t-task: #94a3b8;
|
||||
--ts-t-qa: #fbbf24;
|
||||
|
||||
--pi-color-grey-10: #12141a;
|
||||
--pi-color-grey-25: #161922;
|
||||
--pi-color-grey-50: #1a1e28;
|
||||
--pi-color-grey-75: #1f2430;
|
||||
--pi-color-grey-100: #262c3a;
|
||||
--pi-color-grey-200: #333a4b;
|
||||
--pi-color-grey-300: #4b5265;
|
||||
--pi-color-grey-400: #8a91a4;
|
||||
--pi-color-grey: #a9b0c1;
|
||||
--pi-color-grey-900: #e8eaf1;
|
||||
--pi-color-black: #f4f5f9;
|
||||
--pi-color-black-200: #d8dbe6;
|
||||
--pi-color-background-400: #0f1116;
|
||||
--pi-color-background-300: #333a4b;
|
||||
--pi-color-white-900: #1c202b;
|
||||
|
||||
@include dark-ramp('primary', #ae5de5);
|
||||
@include dark-ramp('accent', #339be6);
|
||||
@include dark-ramp('highlighted', #c79bf0);
|
||||
@include dark-ramp('success', #34c98a);
|
||||
@include dark-ramp('warn', #f07070);
|
||||
|
||||
--pi-theme-field-surface: var(--ts-surface);
|
||||
--pi-theme-field-border-color: #333a4b;
|
||||
--pi-theme-field-border-color-hover: #4b5265;
|
||||
|
||||
background-color: var(--ts-bg);
|
||||
|
||||
// Surfaces that resolve to `--pi-color-white` (which must keep meaning "white"
|
||||
// for text on brand fills), redirected one by one.
|
||||
.pi-card.pi-color__white,
|
||||
.pi-panel.pi-color__white {
|
||||
background-color: var(--ts-surface) !important;
|
||||
}
|
||||
|
||||
.pi-card.pi-color__grey,
|
||||
.pi-list-item.pi-color__grey {
|
||||
background-color: var(--ts-surface-raised) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-notch-piece {
|
||||
background-color: var(--ts-surface) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-dialog-surface {
|
||||
background-color: var(--ts-surface-raised) !important;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user