frontend(page): update page layout, slider and page selector
This commit is contained in:
parent
108cfb9a19
commit
8390ece334
3 changed files with 257 additions and 46 deletions
|
|
@ -7,10 +7,11 @@
|
|||
@for (tower of page().towers; track tower.id) {
|
||||
<lt-tower
|
||||
cdkDrag
|
||||
[cdkDragDisabled]="modalOpen()"
|
||||
[cdkDragDisabled]="modalOpen() || mobileDragDisabled()"
|
||||
[tower]="tower"
|
||||
[dateRange]="dateRange()"
|
||||
[keepTasksOpen]="page().keep_tasks_open"
|
||||
[animateInitialStack]="animateInitialStack()"
|
||||
(cdkDragStarted)="onTowerDragStart(tower.id)"
|
||||
(updateTower)="onUpdateTower(tower.id, $event)"
|
||||
(deleteTowerRequest)="onDeleteTower(tower.id)"
|
||||
|
|
@ -21,7 +22,16 @@
|
|||
}
|
||||
@if (!page().hide_create_tower_button) {
|
||||
<div class="add-tower-wrapper">
|
||||
<img class="add-tower" src="assets/plus-sign.svg" alt="Add tower" (click)="showAddTower.set(true)" />
|
||||
<img
|
||||
class="add-tower"
|
||||
src="assets/plus-sign.svg"
|
||||
alt="Add tower"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
(click)="showAddTower.set(true)"
|
||||
(keydown.enter)="showAddTower.set(true)"
|
||||
(keydown.space)="$event.preventDefault(); showAddTower.set(true)"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
|
@ -48,7 +58,7 @@
|
|||
|
||||
@if (showAddTower()) {
|
||||
<lt-modal title="New Tower" (close)="showAddTower.set(false)">
|
||||
<lt-tower-settings [tower]="null" (save)="onAddTower($event)" />
|
||||
<lt-tower-settings [tower]="null" (save)="onAddTower($event)" (close)="showAddTower.set(false)" />
|
||||
</lt-modal>
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +66,7 @@
|
|||
<lt-modal (close)="cancelTowerDelete()">
|
||||
<div class="confirm-delete">
|
||||
<div class="header">
|
||||
<div class="exit" (click)="cancelTowerDelete()" role="button" aria-label="Cancel"></div>
|
||||
<button class="exit" type="button" (click)="cancelTowerDelete()" aria-label="Cancel"></button>
|
||||
<h2>Delete tower</h2>
|
||||
</div>
|
||||
<p>Delete <strong>{{ confirmDeleteTowerName() || 'this tower' }}</strong> and all of its blocks? This can't be undone.</p>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
@import '../../../styles';
|
||||
@import '../../../library/main';
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
position: relative; // anchor for absolute-positioned .trash
|
||||
|
||||
@include inner-spacing(var(--large-padding));
|
||||
|
|
@ -18,14 +19,17 @@
|
|||
justify-content: center;
|
||||
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
flex: 1 0 auto;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
|
||||
transition: box-shadow $short-animation-time;
|
||||
|
||||
max-width: 800px;
|
||||
max-width: 100%;
|
||||
gap: var(--medium-padding);
|
||||
|
||||
&.cdk-drop-list-dragging {
|
||||
*:not(.cdk-drag-placeholder) {
|
||||
|
|
@ -52,28 +56,45 @@
|
|||
}
|
||||
|
||||
& > * {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
box-sizing: content-box;
|
||||
flex: 0 0 auto;
|
||||
|
||||
&:not(:nth-last-child(1)) {
|
||||
margin-right: var(--medium-padding);
|
||||
@media (max-width: $mobile-width) {
|
||||
margin-right: var(--small-padding);
|
||||
}
|
||||
}
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
position: relative;
|
||||
|
||||
@for $i from 1 to 12 {
|
||||
& > *:first-child:nth-last-child(#{$i}),
|
||||
& > *:first-child:nth-last-child(#{$i}) ~ * {
|
||||
width: calc((100% - (#{$i} - 1) * var(--medium-padding)) / #{$i});
|
||||
// Mobile: fixed-width towers with horizontal scroll (1.5-column rhythm).
|
||||
@media (max-width: $mobile-width) {
|
||||
--mobile-tower-width: calc(66vw - var(--small-padding));
|
||||
--mobile-tower-side-padding: max(
|
||||
var(--medium-padding),
|
||||
calc((100% - var(--mobile-tower-width)) / 2)
|
||||
);
|
||||
|
||||
@media (max-width: $mobile-width) {
|
||||
width: calc((100% - (#{$i} - 1) * var(--small-padding)) / #{$i});
|
||||
}
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-snap-type: x mandatory;
|
||||
scroll-padding-inline: var(--mobile-tower-side-padding);
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
padding: 0 var(--mobile-tower-side-padding);
|
||||
max-width: 100%;
|
||||
gap: var(--medium-padding);
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > * {
|
||||
width: var(--mobile-tower-width) !important;
|
||||
max-width: var(--mobile-tower-width) !important;
|
||||
min-width: var(--mobile-tower-width) !important;
|
||||
scroll-snap-align: center;
|
||||
flex: 0 0 var(--mobile-tower-width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -89,7 +110,11 @@
|
|||
@include card();
|
||||
width: 66vw;
|
||||
max-width: 500px;
|
||||
@media (max-width: $mobile-width) { width: 300px; }
|
||||
@media (max-width: $mobile-width) {
|
||||
width: 88vw;
|
||||
max-width: 88vw;
|
||||
padding: var(--medium-padding);
|
||||
}
|
||||
box-sizing: border-box;
|
||||
padding: var(--large-padding);
|
||||
position: relative;
|
||||
|
|
@ -101,7 +126,7 @@
|
|||
@include center-child();
|
||||
.exit {
|
||||
position: absolute;
|
||||
left: var(--large-padding);
|
||||
right: var(--large-padding);
|
||||
@include exit();
|
||||
}
|
||||
}
|
||||
|
|
@ -115,11 +140,21 @@
|
|||
justify-content: center;
|
||||
gap: var(--large-padding);
|
||||
|
||||
button {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
button.danger {
|
||||
color: #b53f3f;
|
||||
border-bottom-color: #b53f3f55;
|
||||
&:after { background-color: #b53f3f; }
|
||||
}
|
||||
|
||||
@media (max-width: $mobile-width) {
|
||||
flex-direction: column;
|
||||
gap: var(--small-padding);
|
||||
button { width: 100%; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,11 @@ import {
|
|||
input,
|
||||
output,
|
||||
signal,
|
||||
computed,
|
||||
inject,
|
||||
HostListener,
|
||||
effect,
|
||||
untracked,
|
||||
} from '@angular/core';
|
||||
import { Page } from '../../models';
|
||||
import { StoreService } from '../../services/store.service';
|
||||
|
|
@ -15,7 +19,6 @@ import {
|
|||
DoubleSliderComponent,
|
||||
DoubleSliderRange,
|
||||
} from '../shared/double-slider/double-slider.component';
|
||||
import { computed } from '@angular/core';
|
||||
import { CdkDropList, CdkDrag, CdkDragDrop } from '@angular/cdk/drag-drop';
|
||||
import { ModalStateService } from '../../services/modal-state.service';
|
||||
|
||||
|
|
@ -38,6 +41,7 @@ interface BlockPatch {
|
|||
tag: string;
|
||||
description: string;
|
||||
is_done: boolean;
|
||||
difficulty: number;
|
||||
}
|
||||
|
||||
/** Minimum blocks before the date-range slider becomes visible. */
|
||||
|
|
@ -60,12 +64,14 @@ const MIN_BLOCKS_FOR_SLIDER = 2;
|
|||
})
|
||||
export class PageComponent {
|
||||
readonly page = input.required<Page>();
|
||||
readonly animateInitialStack = input<boolean>(false);
|
||||
readonly dragHappening = output<boolean>();
|
||||
|
||||
protected readonly store = inject(StoreService);
|
||||
private readonly modalState = inject(ModalStateService);
|
||||
/** True while any lt-modal is mounted — used to lock tower drag. */
|
||||
readonly modalOpen = this.modalState.anyOpen;
|
||||
readonly mobileDragDisabled = signal(this.isMobileViewport());
|
||||
|
||||
readonly showAddTower = signal(false);
|
||||
readonly isDragging = signal(false);
|
||||
|
|
@ -112,10 +118,30 @@ export class PageComponent {
|
|||
/** Selected date range — `null` = show everything. */
|
||||
readonly dateRange = signal<{ from: number; to: number } | null>(null);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
if (!this.showSlider()) {
|
||||
untracked(() => this.dateRange.set(null));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onSliderRangeChange(range: DoubleSliderRange<unknown>): void {
|
||||
this.dateRange.set({ from: range.from as number, to: range.to as number });
|
||||
}
|
||||
|
||||
@HostListener('window:resize')
|
||||
onResize(): void {
|
||||
this.mobileDragDisabled.set(this.isMobileViewport());
|
||||
}
|
||||
|
||||
private isMobileViewport(): boolean {
|
||||
return (
|
||||
typeof window !== 'undefined' &&
|
||||
window.matchMedia('(max-width: 520px), (pointer: coarse)').matches
|
||||
);
|
||||
}
|
||||
|
||||
// ── Tower mutations ────────────────────────────────────────────────────────
|
||||
|
||||
onAddTower(result: TowerSettingsResult): void {
|
||||
|
|
@ -131,7 +157,7 @@ export class PageComponent {
|
|||
}
|
||||
|
||||
onDeleteTower(towerId: string): void {
|
||||
this.store.deleteTower(this.page().id, towerId);
|
||||
this.confirmDeleteTowerId.set(towerId);
|
||||
}
|
||||
|
||||
// ── Block mutations ────────────────────────────────────────────────────────
|
||||
|
|
@ -143,6 +169,7 @@ export class PageComponent {
|
|||
result.tag,
|
||||
result.description,
|
||||
result.is_done,
|
||||
result.difficulty,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -187,13 +214,16 @@ export class PageComponent {
|
|||
|
||||
onTrashEnter(): void {
|
||||
this.nearTrashcan = true;
|
||||
const preview = document.querySelector('.cdk-drag-preview');
|
||||
if (preview) preview.classList.add('trash-highlight');
|
||||
this.dragPreview()?.classList.add('trash-highlight');
|
||||
}
|
||||
|
||||
onTrashLeave(): void {
|
||||
this.nearTrashcan = false;
|
||||
const preview = document.querySelector('.cdk-drag-preview');
|
||||
if (preview) preview.classList.remove('trash-highlight');
|
||||
this.dragPreview()?.classList.remove('trash-highlight');
|
||||
}
|
||||
|
||||
/** The CDK drag preview currently in flight, if any. Matches legacy DOM-driven trash highlight. */
|
||||
private dragPreview(): Element | null {
|
||||
return document.querySelector('.cdk-drag-preview');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue