Modernise & deploy #1
4 changed files with 178 additions and 198 deletions
|
|
@ -2,6 +2,7 @@ import {
|
||||||
Component,
|
Component,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
output,
|
output,
|
||||||
|
input,
|
||||||
signal,
|
signal,
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
OnDestroy,
|
OnDestroy,
|
||||||
|
|
@ -22,8 +23,20 @@ import { ModalStateService } from '../../services/modal-state.service';
|
||||||
class="modal"
|
class="modal"
|
||||||
[class.active]="active()"
|
[class.active]="active()"
|
||||||
(click)="onBackdropClick($event)"
|
(click)="onBackdropClick($event)"
|
||||||
|
(keydown.enter)="onBackdropClick($any($event))"
|
||||||
|
tabindex="-1"
|
||||||
>
|
>
|
||||||
<div class="modal__dialog" #dialog cdkTrapFocus cdkTrapFocusAutoCapture (keydown.escape)="onClose()">
|
<div
|
||||||
|
class="modal__dialog"
|
||||||
|
#dialog
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
[attr.aria-labelledby]="labelledBy()"
|
||||||
|
[attr.aria-describedby]="describedBy()"
|
||||||
|
cdkTrapFocus
|
||||||
|
cdkTrapFocusAutoCapture
|
||||||
|
(keydown.escape)="onClose()"
|
||||||
|
>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
@ -31,19 +44,35 @@ import { ModalStateService } from '../../services/modal-state.service';
|
||||||
styles: `
|
styles: `
|
||||||
@import '../../../library/main';
|
@import '../../../library/main';
|
||||||
|
|
||||||
section.modal {
|
/* Keep the component host out of parent flex/grid flow. Parent containers
|
||||||
|
apply spacing to direct children, so relying on the fixed child alone can
|
||||||
|
still let <lt-modal> become a layout item when it mounts. */
|
||||||
|
:host {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
inset: 0;
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
|
display: block;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
section.modal {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
@include center-child();
|
@include center-child();
|
||||||
padding: var(--large-padding);
|
padding: var(--large-padding);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: $background-gradient;
|
background: rgba(255, 248, 248, 0.94);
|
||||||
transition: opacity 300ms;
|
transition: opacity 300ms;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
@media (max-width: $mobile-width) {
|
||||||
|
padding: var(--medium-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-height: $min-height) {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
&:not(.active) {
|
&:not(.active) {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
@ -57,6 +86,8 @@ import { ModalStateService } from '../../services/modal-state.service';
|
||||||
`,
|
`,
|
||||||
})
|
})
|
||||||
export class ModalComponent implements AfterViewInit, OnDestroy {
|
export class ModalComponent implements AfterViewInit, OnDestroy {
|
||||||
|
readonly labelledBy = input<string | null>(null);
|
||||||
|
readonly describedBy = input<string | null>(null);
|
||||||
readonly close = output<void>();
|
readonly close = output<void>();
|
||||||
|
|
||||||
// The active signal starts false; AfterViewInit flips it true on next tick
|
// The active signal starts false; AfterViewInit flips it true on next tick
|
||||||
|
|
@ -65,7 +96,6 @@ export class ModalComponent implements AfterViewInit, OnDestroy {
|
||||||
|
|
||||||
private readonly dialogRef = viewChild<ElementRef<HTMLElement>>('dialog');
|
private readonly dialogRef = viewChild<ElementRef<HTMLElement>>('dialog');
|
||||||
private previousFocus: HTMLElement | null = null;
|
private previousFocus: HTMLElement | null = null;
|
||||||
private escListener!: (e: KeyboardEvent) => void;
|
|
||||||
private readonly modalState = inject(ModalStateService);
|
private readonly modalState = inject(ModalStateService);
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
ngAfterViewInit(): void {
|
||||||
|
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
input,
|
|
||||||
output,
|
|
||||||
OnInit,
|
|
||||||
inject,
|
|
||||||
signal,
|
|
||||||
} from '@angular/core';
|
|
||||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
||||||
import { Page } from '../../models';
|
|
||||||
import { ToggleComponent } from '../shared/toggle/toggle.component';
|
|
||||||
|
|
||||||
export interface PageSettingsResult {
|
|
||||||
name: string;
|
|
||||||
hide_create_tower_button: boolean;
|
|
||||||
keep_tasks_open: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'lt-page-settings',
|
|
||||||
standalone: true,
|
|
||||||
imports: [ReactiveFormsModule, ToggleComponent],
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
template: `
|
|
||||||
<div class="header">
|
|
||||||
<div class="exit" (click)="close.emit()" role="button" aria-label="Close"></div>
|
|
||||||
<h2>{{ page() ? 'Page settings' : 'New page' }}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
|
||||||
<input
|
|
||||||
id="ps-name"
|
|
||||||
type="text"
|
|
||||||
formControlName="name"
|
|
||||||
maxlength="200"
|
|
||||||
autocomplete="off"
|
|
||||||
placeholder="Page name…"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="toggle-row">
|
|
||||||
<lt-toggle
|
|
||||||
[checked]="hideCreateTowerButton()"
|
|
||||||
(checkedChange)="hideCreateTowerButton.set($event)"
|
|
||||||
offLabel="Show add-tower button"
|
|
||||||
onLabel="Hide add-tower button"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toggle-row">
|
|
||||||
<lt-toggle
|
|
||||||
[checked]="keepTasksOpen()"
|
|
||||||
(checkedChange)="keepTasksOpen.set($event)"
|
|
||||||
offLabel="Show tasks collapsed"
|
|
||||||
onLabel="Keep tasks open"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button type="submit" [disabled]="form.invalid">
|
|
||||||
{{ page() ? 'Save' : 'Create page' }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if (page()) {
|
|
||||||
<button type="button" (click)="delete.emit()">Delete page</button>
|
|
||||||
}
|
|
||||||
</form>
|
|
||||||
`,
|
|
||||||
styles: `
|
|
||||||
@import '../../../library/main';
|
|
||||||
|
|
||||||
:host {
|
|
||||||
@include card();
|
|
||||||
width: 66vw;
|
|
||||||
max-width: 400px;
|
|
||||||
@media (max-width: $mobile-width) { width: 300px; }
|
|
||||||
box-sizing: border-box;
|
|
||||||
padding: var(--large-padding);
|
|
||||||
position: relative;
|
|
||||||
box-shadow: $shadow;
|
|
||||||
@include inner-spacing(var(--large-padding));
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
.header {
|
|
||||||
@include center-child();
|
|
||||||
|
|
||||||
.exit {
|
|
||||||
position: absolute;
|
|
||||||
left: var(--large-padding);
|
|
||||||
@include exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
input[type='text'] {
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
})
|
|
||||||
export class PageSettingsComponent implements OnInit {
|
|
||||||
readonly page = input<Page | null>(null);
|
|
||||||
readonly save = output<PageSettingsResult>();
|
|
||||||
readonly delete = output<void>();
|
|
||||||
readonly close = output<void>();
|
|
||||||
|
|
||||||
private readonly fb = inject(FormBuilder);
|
|
||||||
|
|
||||||
form = this.fb.group({
|
|
||||||
name: ['', [Validators.required, Validators.maxLength(200)]],
|
|
||||||
});
|
|
||||||
|
|
||||||
hideCreateTowerButton = signal(false);
|
|
||||||
readonly keepTasksOpen = signal(false);
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
const p = this.page();
|
|
||||||
if (p) {
|
|
||||||
this.form.patchValue({ name: p.name });
|
|
||||||
this.hideCreateTowerButton.set(p.hide_create_tower_button);
|
|
||||||
this.keepTasksOpen.set(p.keep_tasks_open);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onSubmit(): void {
|
|
||||||
if (this.form.invalid) return;
|
|
||||||
const v = this.form.value;
|
|
||||||
this.save.emit({
|
|
||||||
name: v.name ?? '',
|
|
||||||
hide_create_tower_button: this.hideCreateTowerButton(),
|
|
||||||
keep_tasks_open: this.keepTasksOpen(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -16,9 +16,6 @@ export interface UpdatePagePayload {
|
||||||
keep_tasks_open: boolean;
|
keep_tasks_open: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UUIDV4_RE =
|
|
||||||
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'lt-settings',
|
selector: 'lt-settings',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
|
|
@ -26,7 +23,7 @@ const UUIDV4_RE =
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<button class="exit" type="button" (click)="close.emit()" aria-label="Close">✕</button>
|
<button class="exit" type="button" (click)="close.emit()" aria-label="Close"></button>
|
||||||
<h2>Settings</h2>
|
<h2>Settings</h2>
|
||||||
|
|
||||||
@if (page()) {
|
@if (page()) {
|
||||||
|
|
@ -36,26 +33,30 @@ const UUIDV4_RE =
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
[value]="pageName()"
|
[value]="pageName()"
|
||||||
(blur)="onRenamePage($any($event.target).value)"
|
(blur)="onRenamePage($any($event.target))"
|
||||||
placeholder="Page name…"
|
placeholder="Page name…"
|
||||||
maxlength="200"
|
maxlength="200"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
aria-label="Page name"
|
aria-label="Page name"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<lt-toggle
|
<div class="toggle-list">
|
||||||
[checked]="hideCreateTowerButton()"
|
<lt-toggle
|
||||||
(checkedChange)="onHideCreateTowerButtonChange($event)"
|
class="setting-toggle"
|
||||||
offLabel="Show add-tower button"
|
[checked]="hideCreateTowerButton()"
|
||||||
onLabel="Hide add-tower button"
|
(checkedChange)="onHideCreateTowerButtonChange($event)"
|
||||||
/>
|
offLabel="Show add-tower button"
|
||||||
|
onLabel="Hide add-tower button"
|
||||||
|
/>
|
||||||
|
|
||||||
<lt-toggle
|
<lt-toggle
|
||||||
[checked]="keepTasksOpen()"
|
class="setting-toggle"
|
||||||
(checkedChange)="onKeepTasksOpenChange($event)"
|
[checked]="keepTasksOpen()"
|
||||||
offLabel="Show tasks collapsed"
|
(checkedChange)="onKeepTasksOpenChange($event)"
|
||||||
onLabel="Keep tasks open"
|
offLabel="Show tasks collapsed"
|
||||||
/>
|
onLabel="Keep tasks open"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="danger" type="button" (click)="deletePage.emit()">
|
<button class="danger" type="button" (click)="deletePage.emit()">
|
||||||
Delete this page
|
Delete this page
|
||||||
|
|
@ -68,7 +69,7 @@ const UUIDV4_RE =
|
||||||
<section class="account-section">
|
<section class="account-section">
|
||||||
<h3>Account</h3>
|
<h3>Account</h3>
|
||||||
|
|
||||||
<p class="hint">Your token (keep it secret — it IS your account)</p>
|
<p class="hint">Copy this token to another device to permanently sync your progress</p>
|
||||||
<div class="token-row">
|
<div class="token-row">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -111,7 +112,11 @@ const UUIDV4_RE =
|
||||||
@include card();
|
@include card();
|
||||||
width: 66vw;
|
width: 66vw;
|
||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
@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;
|
box-sizing: border-box;
|
||||||
padding: var(--large-padding);
|
padding: var(--large-padding);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -123,17 +128,19 @@ const UUIDV4_RE =
|
||||||
top: var(--medium-padding);
|
top: var(--medium-padding);
|
||||||
right: var(--medium-padding);
|
right: var(--medium-padding);
|
||||||
@include exit();
|
@include exit();
|
||||||
font-size: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin: 0 0 var(--large-padding) 0;
|
margin: 0 0 var(--large-padding) 0;
|
||||||
|
padding: 0 36px;
|
||||||
|
line-height: 1.3;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
margin: 0 0 var(--medium-padding) 0;
|
margin: 0 0 var(--medium-padding) 0;
|
||||||
font-size: var(--large-font-size);
|
font-size: var(--medium-font-size);
|
||||||
|
line-height: 1.35;
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
|
|
@ -151,8 +158,37 @@ const UUIDV4_RE =
|
||||||
margin: var(--large-padding) 0;
|
margin: var(--large-padding) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input[type='text'],
|
||||||
|
button:not(.exit) {
|
||||||
|
font-size: var(--medium-font-size);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--small-padding);
|
||||||
|
}
|
||||||
|
|
||||||
|
lt-toggle.setting-toggle {
|
||||||
|
--toggle-label-width: 145px;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 52px;
|
||||||
|
padding: var(--small-padding);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background: rgba($text-color, 0.035);
|
||||||
|
|
||||||
|
@media (max-width: $mobile-width) {
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
font-size: var(--small-font-size);
|
font-size: var(--medium-font-size);
|
||||||
|
line-height: 1.35;
|
||||||
color: rgba($text-color, 0.7);
|
color: rgba($text-color, 0.7);
|
||||||
margin: 0 0 4px 0;
|
margin: 0 0 4px 0;
|
||||||
}
|
}
|
||||||
|
|
@ -171,6 +207,13 @@ const UUIDV4_RE =
|
||||||
button {
|
button {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: $mobile-width) {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
input { width: 100%; }
|
||||||
|
button { margin-left: auto; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -209,10 +252,15 @@ export class SettingsComponent {
|
||||||
readonly hideCreateTowerButton = signal(false);
|
readonly hideCreateTowerButton = signal(false);
|
||||||
readonly keepTasksOpen = signal(false);
|
readonly keepTasksOpen = signal(false);
|
||||||
|
|
||||||
|
private static readonly UUIDV4_RE =
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||||
|
|
||||||
// Token-switch state
|
// Token-switch state
|
||||||
readonly tokenInput = signal('');
|
readonly tokenInput = signal('');
|
||||||
readonly tokenInputTouched = signal(false);
|
readonly tokenInputTouched = signal(false);
|
||||||
readonly isValidToken = computed(() => UUIDV4_RE.test(this.tokenInput()));
|
readonly isValidToken = computed(() =>
|
||||||
|
SettingsComponent.UUIDV4_RE.test(this.tokenInput()),
|
||||||
|
);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
effect(() => {
|
effect(() => {
|
||||||
|
|
@ -225,10 +273,14 @@ export class SettingsComponent {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onRenamePage(value: string): void {
|
onRenamePage(input: HTMLInputElement): void {
|
||||||
const trimmed = value.trim();
|
const trimmed = input.value.trim();
|
||||||
if (!trimmed) return;
|
if (!trimmed) {
|
||||||
|
input.value = this.pageName();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.pageName.set(trimmed);
|
this.pageName.set(trimmed);
|
||||||
|
input.value = trimmed;
|
||||||
this.flushPageUpdate();
|
this.flushPageUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,9 @@ import {
|
||||||
output,
|
output,
|
||||||
OnInit,
|
OnInit,
|
||||||
inject,
|
inject,
|
||||||
|
DestroyRef,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { Tower, HslColor } from '../../models';
|
import { Tower, HslColor } from '../../models';
|
||||||
import { ColorPickerComponent } from '../shared/color-picker/color-picker.component';
|
import { ColorPickerComponent } from '../shared/color-picker/color-picker.component';
|
||||||
|
|
@ -21,10 +23,7 @@ export interface TowerSettingsResult {
|
||||||
imports: [ReactiveFormsModule, ColorPickerComponent],
|
imports: [ReactiveFormsModule, ColorPickerComponent],
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
template: `
|
template: `
|
||||||
<div class="header">
|
<button class="exit" type="button" (click)="close.emit()" aria-label="Close"></button>
|
||||||
<div class="exit" (click)="close.emit()" role="button" aria-label="Close"></div>
|
|
||||||
<h2>{{ tower() ? 'Tower settings' : 'New tower' }}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
<form [formGroup]="form" (ngSubmit)="onSubmit()">
|
||||||
<input
|
<input
|
||||||
|
|
@ -32,19 +31,22 @@ export interface TowerSettingsResult {
|
||||||
name="towerName"
|
name="towerName"
|
||||||
type="text"
|
type="text"
|
||||||
formControlName="name"
|
formControlName="name"
|
||||||
placeholder="Tower name…"
|
[placeholder]="tower() ? 'Tower name…' : 'New tower'"
|
||||||
maxlength="200"
|
maxlength="200"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
|
class="title-input"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<lt-color-picker [color]="currentColor" (colorChange)="onColorChange($event)" />
|
<div class="picker-row">
|
||||||
|
<lt-color-picker [color]="currentColor" (colorChange)="onColorChange($event)" />
|
||||||
<button type="submit" [disabled]="form.invalid">
|
</div>
|
||||||
{{ tower() ? 'Save' : 'Create tower' }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
@if (tower()) {
|
@if (tower()) {
|
||||||
|
<!-- Editing an existing tower: changes auto-save, so there's no Save
|
||||||
|
button — only the destructive action remains explicit. -->
|
||||||
<button type="button" (click)="delete.emit()">Delete tower</button>
|
<button type="button" (click)="delete.emit()">Delete tower</button>
|
||||||
|
} @else {
|
||||||
|
<button type="submit" [disabled]="form.invalid">Create tower</button>
|
||||||
}
|
}
|
||||||
</form>
|
</form>
|
||||||
`,
|
`,
|
||||||
|
|
@ -55,30 +57,45 @@ export interface TowerSettingsResult {
|
||||||
@include card();
|
@include card();
|
||||||
width: 66vw;
|
width: 66vw;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
@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;
|
box-sizing: border-box;
|
||||||
padding: var(--large-padding);
|
padding: var(--large-padding);
|
||||||
|
padding-top: calc(var(--large-padding) + var(--medium-padding));
|
||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: $shadow;
|
box-shadow: $shadow;
|
||||||
@include inner-spacing(var(--large-padding));
|
|
||||||
display: block;
|
display: block;
|
||||||
|
|
||||||
.header {
|
.exit {
|
||||||
@include center-child();
|
position: absolute;
|
||||||
|
top: var(--medium-padding);
|
||||||
.exit {
|
right: var(--medium-padding);
|
||||||
position: absolute;
|
@include exit();
|
||||||
left: var(--large-padding);
|
|
||||||
@include exit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type='text'] {
|
form {
|
||||||
|
@include inner-spacing(var(--large-padding));
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-input {
|
||||||
|
@include title-text();
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
display: block;
|
display: block;
|
||||||
|
// Stay full-width on mobile, but switch to flex so forms.scss's
|
||||||
|
// bottom-alignment keeps the underline hugging the label in the 42px
|
||||||
|
// tap target (plain block centres the text and strands the underline).
|
||||||
|
@media (max-width: $mobile-width) {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
@ -90,6 +107,7 @@ export class TowerSettingsComponent implements OnInit {
|
||||||
readonly close = output<void>();
|
readonly close = output<void>();
|
||||||
|
|
||||||
private readonly fb = inject(FormBuilder);
|
private readonly fb = inject(FormBuilder);
|
||||||
|
private readonly destroyRef = inject(DestroyRef);
|
||||||
|
|
||||||
form = this.fb.group({
|
form = this.fb.group({
|
||||||
name: ['', [Validators.required, Validators.maxLength(200)]],
|
name: ['', [Validators.required, Validators.maxLength(200)]],
|
||||||
|
|
@ -102,17 +120,38 @@ export class TowerSettingsComponent implements OnInit {
|
||||||
if (t) {
|
if (t) {
|
||||||
this.form.patchValue({ name: t.name });
|
this.form.patchValue({ name: t.name });
|
||||||
this.currentColor = { ...t.base_color };
|
this.currentColor = { ...t.base_color };
|
||||||
|
|
||||||
|
// Edit mode: persist name changes as they happen. Wire this up *after*
|
||||||
|
// the initial patchValue so seeding the form doesn't fire a save.
|
||||||
|
this.form.valueChanges
|
||||||
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
|
.subscribe(() => this.autoSave());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onColorChange(color: HslColor): void {
|
onColorChange(color: HslColor): void {
|
||||||
this.currentColor = color;
|
this.currentColor = color;
|
||||||
|
// In edit mode the picker is a live control — commit each change.
|
||||||
|
if (this.tower()) this.autoSave();
|
||||||
}
|
}
|
||||||
|
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
|
// Only the create flow reaches here via its Submit button; edit mode
|
||||||
|
// auto-saves (and Enter on the single field is a harmless redundant save).
|
||||||
|
this.tryEmitSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
private autoSave(): void {
|
||||||
|
this.tryEmitSave();
|
||||||
|
}
|
||||||
|
|
||||||
|
private tryEmitSave(): void {
|
||||||
if (this.form.invalid) return;
|
if (this.form.invalid) return;
|
||||||
const v = this.form.value;
|
this.emitSave();
|
||||||
this.save.emit({ name: v.name ?? '', base_color: this.currentColor });
|
}
|
||||||
|
|
||||||
|
private emitSave(): void {
|
||||||
|
this.save.emit({ name: this.form.value.name ?? '', base_color: this.currentColor });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue