UUIDV4_RE.test(this.tokenInput()));
+ readonly isValidToken = computed(() =>
+ SettingsComponent.UUIDV4_RE.test(this.tokenInput()),
+ );
constructor() {
effect(() => {
@@ -225,10 +273,14 @@ export class SettingsComponent {
});
}
- onRenamePage(value: string): void {
- const trimmed = value.trim();
- if (!trimmed) return;
+ onRenamePage(input: HTMLInputElement): void {
+ const trimmed = input.value.trim();
+ if (!trimmed) {
+ input.value = this.pageName();
+ return;
+ }
this.pageName.set(trimmed);
+ input.value = trimmed;
this.flushPageUpdate();
}
diff --git a/frontend/src/app/components/modal/tower-settings.component.ts b/frontend/src/app/components/modal/tower-settings.component.ts
index 4d99802..7443e40 100644
--- a/frontend/src/app/components/modal/tower-settings.component.ts
+++ b/frontend/src/app/components/modal/tower-settings.component.ts
@@ -5,7 +5,9 @@ import {
output,
OnInit,
inject,
+ DestroyRef,
} from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { Tower, HslColor } from '../../models';
import { ColorPickerComponent } from '../shared/color-picker/color-picker.component';
@@ -21,10 +23,7 @@ export interface TowerSettingsResult {
imports: [ReactiveFormsModule, ColorPickerComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
-
+
`,
@@ -55,30 +57,45 @@ export interface TowerSettingsResult {
@include card();
width: 66vw;
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;
padding: var(--large-padding);
+ padding-top: calc(var(--large-padding) + var(--medium-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();
- }
+ .exit {
+ position: absolute;
+ top: var(--medium-padding);
+ right: var(--medium-padding);
+ @include exit();
}
- input[type='text'] {
+ form {
+ @include inner-spacing(var(--large-padding));
+ }
+
+ .title-input {
+ @include title-text();
text-align: center;
+ width: 100%;
+ background: transparent;
+ border: 0;
}
button {
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
();
private readonly fb = inject(FormBuilder);
+ private readonly destroyRef = inject(DestroyRef);
form = this.fb.group({
name: ['', [Validators.required, Validators.maxLength(200)]],
@@ -102,17 +120,38 @@ export class TowerSettingsComponent implements OnInit {
if (t) {
this.form.patchValue({ name: t.name });
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 {
this.currentColor = color;
+ // In edit mode the picker is a live control — commit each change.
+ if (this.tower()) this.autoSave();
}
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;
- const v = this.form.value;
- this.save.emit({ name: v.name ?? '', base_color: this.currentColor });
+ this.emitSave();
+ }
+
+ private emitSave(): void {
+ this.save.emit({ name: this.form.value.name ?? '', base_color: this.currentColor });
}
}