This commit is contained in:
Andras Schmelczer 2026-05-30 14:47:25 +01:00
parent 003f38ea60
commit f87480e79d
7 changed files with 124 additions and 36 deletions

View file

@ -5,7 +5,8 @@
"packageManager": "npm", "packageManager": "npm",
"schematicCollections": [ "schematicCollections": [
"angular-eslint" "angular-eslint"
] ],
"analytics": false
}, },
"newProjectRoot": "projects", "newProjectRoot": "projects",
"projects": { "projects": {

View file

@ -74,6 +74,7 @@ interface EditedValue {
[alwaysDropShadow]="true" [alwaysDropShadow]="true"
[onlyShadowBorder]="true" [onlyShadowBorder]="true"
placeholder="Tag this item…" placeholder="Tag this item…"
emptyHint="No tags yet. Type one below to create it."
(select)="updateTag(b.id, $event)" (select)="updateTag(b.id, $event)"
(add)="updateTag(b.id, $event)" (add)="updateTag(b.id, $event)"
/> />
@ -125,6 +126,7 @@ interface EditedValue {
[alwaysDropShadow]="true" [alwaysDropShadow]="true"
[onlyShadowBorder]="true" [onlyShadowBorder]="true"
placeholder="Set a category…" placeholder="Set a category…"
emptyHint="No tags yet. Type one below to create it."
(select)="updateNewTag($event)" (select)="updateNewTag($event)"
(add)="updateNewTag($event)" (add)="updateNewTag($event)"
/> />

View file

@ -28,7 +28,8 @@
transition: box-shadow $short-animation-time; transition: box-shadow $short-animation-time;
max-width: 800px; max-width: 100%;
gap: var(--medium-padding);
&.cdk-drop-list-dragging { &.cdk-drop-list-dragging {
*:not(.cdk-drag-placeholder) { *:not(.cdk-drag-placeholder) {
@ -55,38 +56,17 @@
} }
& > * { & > * {
width: 100%;
max-width: 200px; max-width: 200px;
box-sizing: content-box; box-sizing: border-box;
flex: 0 0 auto; flex: 1 1 0;
min-width: 0;
min-height: 0; min-height: 0;
&:not(:nth-last-child(1)) {
margin-right: var(--medium-padding);
@media (max-width: $mobile-width) {
// Was --small-padding (7.5px) too tight to read as a real gap
// between white tower cards on the cream-pink background.
margin-right: var(--medium-padding);
}
}
} }
position: relative; 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});
@media (max-width: $mobile-width) {
width: calc((100% - (#{$i} - 1) * var(--small-padding)) / #{$i});
}
}
}
// Mobile: fixed-width towers with horizontal scroll (1.5-column rhythm). // Mobile: fixed-width towers with horizontal scroll (1.5-column rhythm).
// This block is declared AFTER the @for loop so it wins the specificity race
// without needing !important on every property only the width triples need it
// to beat the per-child-count selectors generated above.
@media (max-width: $mobile-width) { @media (max-width: $mobile-width) {
--mobile-tower-width: calc(66vw - var(--small-padding)); --mobile-tower-width: calc(66vw - var(--small-padding));
--mobile-tower-side-padding: max( --mobile-tower-side-padding: max(
@ -109,17 +89,12 @@
display: none; display: none;
} }
// Override the @for width-calc rules above.
& > * { & > * {
width: var(--mobile-tower-width) !important; width: var(--mobile-tower-width) !important;
max-width: var(--mobile-tower-width) !important; max-width: var(--mobile-tower-width) !important;
min-width: var(--mobile-tower-width) !important; min-width: var(--mobile-tower-width) !important;
scroll-snap-align: center; scroll-snap-align: center;
flex-shrink: 0; flex: 0 0 var(--mobile-tower-width);
}
& > *:not(:nth-last-child(1)) {
margin-right: 0;
} }
} }
} }

View file

@ -27,12 +27,31 @@
[page]="selectedPage()" [page]="selectedPage()"
(close)="showSettings.set(false)" (close)="showSettings.set(false)"
(updatePage)="onUpdatePage($event)" (updatePage)="onUpdatePage($event)"
(deletePage)="onRemovePage()" (deletePage)="onRequestRemovePage()"
(switchAccount)="onSwitchAccount($event)" (switchAccount)="onSwitchAccount($event)"
/> />
</lt-modal> </lt-modal>
} }
@if (confirmDeletePageId()) {
<lt-modal (close)="cancelRemovePage()">
<div class="confirm-delete">
<div class="header">
<button class="exit" type="button" (click)="cancelRemovePage()" aria-label="Cancel"></button>
<h2>Delete page</h2>
</div>
<p>
Delete <strong>{{ confirmDeletePageName() || 'this page' }}</strong> and all of its towers and blocks?
This can't be undone.
</p>
<div class="confirm-buttons">
<button type="button" (click)="cancelRemovePage()">Cancel</button>
<button type="button" class="danger" (click)="confirmRemovePage()">Delete page</button>
</div>
</div>
</lt-modal>
}
@if (showWelcome()) { @if (showWelcome()) {
<lt-modal (close)="showWelcome.set(false)"> <lt-modal (close)="showWelcome.set(false)">
<lt-welcome <lt-welcome

View file

@ -44,4 +44,65 @@
font-size: var(--medium-font-size); font-size: var(--medium-font-size);
} }
} }
.confirm-delete {
@include card();
width: 66vw;
max-width: 500px;
@media (max-width: $mobile-width) {
width: 88vw;
max-width: 88vw;
padding: var(--medium-padding);
}
box-sizing: border-box;
padding: var(--large-padding);
position: relative;
box-shadow: $shadow;
@include inner-spacing(var(--large-padding));
text-align: center;
.header {
@include center-child();
.exit {
position: absolute;
right: var(--large-padding);
@include exit();
}
}
p {
strong {
font-weight: bold;
}
}
.confirm-buttons {
display: flex;
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%;
}
}
}
}
} }

View file

@ -31,6 +31,7 @@ export class PagesComponent {
readonly showSettings = signal(false); readonly showSettings = signal(false);
readonly dragHappening = signal(false); readonly dragHappening = signal(false);
readonly showWelcome = signal(false); readonly showWelcome = signal(false);
readonly confirmDeletePageId = signal<string | null>(null);
constructor() { constructor() {
effect(() => { effect(() => {
@ -63,6 +64,12 @@ export class PagesComponent {
readonly selectedPageName = computed(() => this.selectedPage()?.name ?? null); readonly selectedPageName = computed(() => this.selectedPage()?.name ?? null);
readonly confirmDeletePageName = computed(() => {
const id = this.confirmDeletePageId();
if (!id) return '';
return this.store.pages().find((p) => p.id === id)?.name ?? '';
});
readonly selectedPageIndex = computed(() => { readonly selectedPageIndex = computed(() => {
const page = this.selectedPage(); const page = this.selectedPage();
if (!page) return -1; if (!page) return -1;
@ -94,12 +101,23 @@ export class PagesComponent {
} }
} }
onRemovePage(): void { onRequestRemovePage(): void {
const page = this.selectedPage(); const page = this.selectedPage();
if (!page) return; if (!page) return;
this.store.deletePage(page.id); this.confirmDeletePageId.set(page.id);
}
confirmRemovePage(): void {
const pageId = this.confirmDeletePageId();
if (!pageId) return;
this.store.deletePage(pageId);
this.selectedPageId.set(null); this.selectedPageId.set(null);
this.showSettings.set(false); this.showSettings.set(false);
this.confirmDeletePageId.set(null);
}
cancelRemovePage(): void {
this.confirmDeletePageId.set(null);
} }
onSwitchAccount(token: string): void { onSwitchAccount(token: string): void {

View file

@ -28,6 +28,9 @@ import {
</div> </div>
<div class="bottom-container"> <div class="bottom-container">
<div class="bottom" [class.open]="open()"> <div class="bottom" [class.open]="open()">
@if (resolvedItems().length === 0 && emptyHint()) {
<p class="empty-hint">{{ emptyHint() }}</p>
}
@for (item of resolvedItems(); track item) { @for (item of resolvedItems(); track item) {
@if (editing()) { @if (editing()) {
<input <input
@ -201,6 +204,14 @@ import {
} }
} }
.empty-hint {
@include medium-text();
width: 100%;
margin: 0;
color: rgba($text-color, 0.72);
text-align: left;
}
input[type='text'] { input[type='text'] {
@include sub-title-text(); @include sub-title-text();
width: 100%; width: 100%;
@ -373,6 +384,7 @@ export class SelectAddComponent implements OnChanges {
readonly selected = input<string | null>(null); readonly selected = input<string | null>(null);
readonly editable = input<boolean>(false); readonly editable = input<boolean>(false);
readonly placeholder = input<string>('Select…'); readonly placeholder = input<string>('Select…');
readonly emptyHint = input<string>('');
readonly alwaysDropShadow = input<boolean>(false); readonly alwaysDropShadow = input<boolean>(false);
readonly onlyShadowBorder = input<boolean>(false); readonly onlyShadowBorder = input<boolean>(false);