Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ export class MyComponent extends AsyncHandler {
- Do not add Angular Material. Use the controls in `src/app/ui/controls` (form field, select, autocomplete, chip list, tabs, tooltip, spinner, progress bar, dialog close, toasts) and the Angular CDK (dialog, menu, overlay). Their styles are in `src/styles.css`
- Form errors: put `<div error>` in an `app-form-field`. It shows when its `[formField]` or `ngModel` control is touched and invalid
- Add a `ripple` attribute to an element for a press ripple. Use native `<input type="checkbox">` for checkboxes, with `role="switch"` for a switch
- Motion: each route change is a view transition (styles in `src/styles.css`). Add a `page-transition` attribute to a routed content area to crossfade it. For enter motion use the `animate-fade-in`, `animate-dialog-enter` and `animate-slide-in-up` classes. For exit motion use `animate.leave`. Stop motion that moves or scales for `prefers-reduced-motion`
- Use **IconComponent** for icons. The default icon font is a subset of the icons in the source. After you add icons, run `bun config/icon-subset.ts` (needs `pip install fonttools brotli`). An icon that is not in the subset still shows, but it loads the full 420 KB font
- `src/index.html` has a splash that shows until the app loads. Lighthouse measures its title as the largest contentful paint, so keep the title larger than the content of the first page
- Use `feat/*`, `fix/*`, `perf/*`, or `refactor/*` for branch names
Expand Down
2 changes: 2 additions & 0 deletions src/app/admin/admin.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { TranslatePipe } from '../ui/translate.pipe';
<div class="flex h-full w-px flex-1 flex-col overflow-hidden">
<div class="z-0 flex h-1/2 flex-1">
<nav
admin-nav
class="border-base-200 relative z-10 h-full space-y-2 overflow-auto border-r px-2 pt-10 sm:w-56 sm:py-4"
[attr.aria-label]="'COMMON.MANAGE' | translate"
>
Expand All @@ -47,6 +48,7 @@ import { TranslatePipe } from '../ui/translate.pipe';
<main
id="main-content"
tabindex="-1"
page-transition
class="relative z-0 h-full w-1/2 flex-1 overflow-auto outline-none"
>
<router-outlet />
Expand Down
26 changes: 26 additions & 0 deletions src/app/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,31 @@ import {
provideZonelessChangeDetection,
} from '@angular/core';
import {
ActivatedRouteSnapshot,
provideRouter,
withHashLocation,
withNavigationErrorHandler,
withViewTransitions,
} from '@angular/router';
import { provideServiceWorker } from '@angular/service-worker';
import { appRoutes } from './app.routes';
import { reloadApplicationOnChunkLoadError } from './common/application';
import { SentryReportingErrorHandler } from './common/sentry';

/** Upper bound of route levels that `routePath` walks */
const MAX_ROUTE_DEPTH = 32;

/** Path of a router state without its query params, e.g. `systems/sys-1/about` */
function routePath(route: ActivatedRouteSnapshot) {
let leaf = route;
for (let i = 0; leaf.firstChild && i < MAX_ROUTE_DEPTH; i++) {
leaf = leaf.firstChild;
}
return leaf.pathFromRoot
.flatMap((level) => level.url.map((segment) => segment.path))
.join('/');
}

export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
Expand All @@ -26,6 +42,16 @@ export const appConfig: ApplicationConfig = {
withNavigationErrorHandler((event) =>
reloadApplicationOnChunkLoadError(event.error),
),
// Styles for the transitions are in styles.css. A change of
// query params only, e.g. a filter, does not animate.
withViewTransitions({
skipInitialTransition: true,
onViewTransitionCreated: ({ transition, from, to }) => {
if (routePath(from) === routePath(to)) {
transition.skipTransition();
}
},
}),
),
provideServiceWorker('ngsw-worker.js', {
enabled: !isDevMode(), // Disable in development, enable in production
Expand Down
6 changes: 5 additions & 1 deletion src/app/ui/controls/toast-outlet.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import { dismissToast, Toast, toasts } from '../../common/notifications';
<ng-template #panel>
<div class="toast-stack">
@for (toast of list(); track toast.id) {
<div class="toast" [class]="toast.type">
<div
class="toast"
[class]="toast.type"
animate.leave="toast-leave"
>
<span class="toast-message">{{ toast.message }}</span>
@if (toast.action) {
<button type="button" (click)="act(toast)">
Expand Down
2 changes: 1 addition & 1 deletion src/app/ui/global-banner.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface BannerDetails {
template: `
@if (!has_viewed) {
<div
class="flex w-full items-center space-x-4 p-4"
class="animate-fade-in flex w-full items-center space-x-4 p-4"
[class.bg-info]="banner.type === 'info' || !banner.type"
[class.text-info-content]="
banner.type === 'info' || !banner.type
Expand Down
4 changes: 2 additions & 2 deletions src/app/ui/item-selection.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ import { VirtualScrollComponent } from './virtual-scroll.component';
</div>
@if (show_view()) {
<button
class="bg-base-100/80 absolute inset-0"
class="animate-fade-in bg-base-100/80 absolute inset-0"
(click)="close()"
>
<button
class="bg-base-100 border-base-300 absolute top-2 left-1/2 flex w-lg max-w-[calc(100%-1rem)] -translate-x-1/2 flex-col space-y-2 overflow-hidden rounded-sm border shadow-sm"
class="animate-dialog-enter bg-base-100 border-base-300 absolute top-2 left-1/2 flex w-lg max-w-[calc(100%-1rem)] -translate-x-1/2 flex-col space-y-2 overflow-hidden rounded-sm border shadow-sm"
(click)="$event.stopPropagation()"
>
<div
Expand Down
8 changes: 5 additions & 3 deletions src/app/ui/item-shell.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,10 @@ function mediaQuery(query: string) {
@if (loading()) {
<item-details-skeleton />
} @else if (display_item()?.id) {
<!-- Tabs and menus load once an item shows -->
<!-- Tabs and menus load once an item shows. They fade in over the skeleton. -->
@defer (on immediate) {
<item-details
class="animate-fade-in"
[item]="display_item()"
[extra_actions]="extra_actions()"
[type]="
Expand All @@ -103,11 +104,12 @@ function mediaQuery(query: string) {
[base]="name()"
[tabs]="tabs()"
[scrolled]="scroll() > 0"
class="z-10"
class="animate-fade-in z-10"
/>
<div
#el
class="relative z-0 h-1/2 w-full flex-1 overflow-auto"
page-transition
class="animate-fade-in relative z-0 h-1/2 w-full flex-1 overflow-auto"
(scroll)="scroll.set(el.scrollTop)"
>
<router-outlet />
Expand Down
17 changes: 14 additions & 3 deletions src/app/ui/item-tablist.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export interface ItemTab {
#rla="routerLinkActive"
[attr.aria-current]="rla.isActive ? 'page' : null"
>
<!-- Moves to the new tab in the page transition -->
<span indicator></span>
<icon class="text-xl" [icon]="link.icon" />&nbsp;
<div class="name">{{ link.name }}&nbsp;</div>
@if (link.count || link.count === 0) {
Expand All @@ -79,7 +81,9 @@ export interface ItemTab {
class="h-3 w-3 animate-spin rounded-full border-2 border-current/30 border-t-current"
></span>
} @else {
{{ link.count || '0' }}
<span class="animate-fade-in">{{
link.count || '0'
}}</span>
}
</div>
}
Expand Down Expand Up @@ -123,6 +127,7 @@ export interface ItemTab {
}

a {
position: relative;
display: flex;
flex: 1 0 auto;
align-items: center;
Expand All @@ -133,7 +138,6 @@ export interface ItemTab {
letter-spacing: 0.09em;
white-space: nowrap;
color: var(--base-content);
border-bottom: 2px solid transparent;
}

a:hover {
Expand All @@ -146,7 +150,14 @@ export interface ItemTab {

a.active {
color: var(--secondary);
border-bottom-color: var(--secondary);
}

a.active [indicator] {
position: absolute;
inset: auto 0 0;
height: 2px;
background-color: var(--secondary);
view-transition-name: tab-indicator;
}
`,
],
Expand Down
14 changes: 14 additions & 0 deletions src/app/ui/sidebar-menu.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { UserMenuTooltipComponent } from './user-menu-tooltip.component';
[class.compact]="compact()"
[class.sm:w-52]="!compact()"
[class.!flex]="open()"
[class.open]="open()"
[class.pointer-events-auto!]="open()"
>
<nav
Expand Down Expand Up @@ -267,6 +268,19 @@ import { UserMenuTooltipComponent } from './user-menu-tooltip.component';
width: 4.5rem;
}
}

/* On small screens the menu covers the page. It slides in when it opens. */
@media (max-width: 39.99rem) and (prefers-reduced-motion: no-preference) {
.open {
animation: menu-enter 200ms var(--ease-enter);
}
}

@keyframes menu-enter {
from {
transform: translateX(-100%);
}
}
`,
],
imports: [
Expand Down
2 changes: 1 addition & 1 deletion src/app/ui/upload-list.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { TranslatePipe } from './translate.pipe';
@if (show()) {
<div
upload-list
class="border-base-300 bg-base-100 text-base-content pointer-events-auto absolute bottom-2 left-2 overflow-hidden rounded-sm border text-sm shadow-sm"
class="animate-slide-in-up border-base-300 bg-base-100 text-base-content pointer-events-auto absolute bottom-2 left-2 overflow-hidden rounded-sm border text-sm shadow-sm"
>
<div
class="bg-base-200 text-base-content flex items-center gap-2 p-2"
Expand Down
Loading
Loading