getting there

This commit is contained in:
Andras Schmelczer 2026-05-30 15:40:53 +01:00
parent f87480e79d
commit 5ac6633d40
20 changed files with 898 additions and 112 deletions

View file

@ -23,12 +23,14 @@ export interface BlockEditSave {
tag: string;
description: string;
is_done: boolean;
difficulty: number;
}
interface EditedValue {
tag: string;
description: string;
is_done: boolean;
difficulty: number;
}
@Component({
@ -73,8 +75,7 @@ interface EditedValue {
[selected]="editedFor(b.id).tag"
[alwaysDropShadow]="true"
[onlyShadowBorder]="true"
placeholder="Tag this item…"
emptyHint="No tags yet. Type one below to create it."
[placeholder]="tagPlaceholder('Tag this item…')"
(select)="updateTag(b.id, $event)"
(add)="updateTag(b.id, $event)"
/>
@ -96,6 +97,26 @@ interface EditedValue {
<span>Already done</span>
</label>
<div class="difficulty">
<span class="label">Difficulty</span>
<div class="stepper">
<button
type="button"
class="step"
aria-label="Decrease difficulty"
[disabled]="editedFor(b.id).difficulty <= 1"
(click)="updateDifficulty(b.id, -1); $event.stopPropagation()"
></button>
<span class="value">{{ editedFor(b.id).difficulty }}</span>
<button
type="button"
class="step"
aria-label="Increase difficulty"
(click)="updateDifficulty(b.id, 1); $event.stopPropagation()"
>+</button>
</div>
</div>
<div class="bottom">
<button (click)="onDelete(b.id); $event.stopPropagation()">Delete</button>
</div>
@ -125,8 +146,7 @@ interface EditedValue {
[selected]="newValue().tag"
[alwaysDropShadow]="true"
[onlyShadowBorder]="true"
placeholder="Set a category…"
emptyHint="No tags yet. Type one below to create it."
[placeholder]="tagPlaceholder('Set a category…')"
(select)="updateNewTag($event)"
(add)="updateNewTag($event)"
/>
@ -147,6 +167,26 @@ interface EditedValue {
<span>Already done</span>
</label>
<div class="difficulty">
<span class="label">Difficulty</span>
<div class="stepper">
<button
type="button"
class="step"
aria-label="Decrease difficulty"
[disabled]="newValue().difficulty <= 1"
(click)="updateNewDifficulty(-1); $event.stopPropagation()"
></button>
<span class="value">{{ newValue().difficulty }}</span>
<button
type="button"
class="step"
aria-label="Increase difficulty"
(click)="updateNewDifficulty(1); $event.stopPropagation()"
>+</button>
</div>
</div>
<div class="bottom">
<button
(click)="submitNew(); $event.stopPropagation()"
@ -394,6 +434,68 @@ interface EditedValue {
}
}
.difficulty {
@include medium-text();
display: flex;
align-items: center;
justify-content: center;
gap: var(--small-padding);
width: max-content;
max-width: 100%;
margin: 0 auto var(--medium-padding);
.stepper {
display: flex;
align-items: center;
gap: var(--small-padding);
.value {
min-width: 1.5em;
text-align: center;
font-variant-numeric: tabular-nums;
}
button.step {
all: unset; // strip native + global button styles
@include square(22px);
@include center-child();
flex: 0 0 auto;
box-sizing: border-box;
border-radius: 4px;
background: $light-color;
box-shadow: $shadow-border;
cursor: pointer;
// all:unset drops the font to serif and the global button's hover
// underline (button::after) survives the reset — re-assert both.
font: bold 18px/1 $normal-font;
color: $text-color;
user-select: none;
transition: box-shadow $long-animation-time, transform $short-animation-time, opacity $short-animation-time;
&::after { content: none; }
@media (max-width: $mobile-width) {
@include square(26px);
}
&:hover,
&:focus-visible {
box-shadow: $shadow;
}
&:active {
transform: scale(0.95);
}
&:disabled {
opacity: 0.4;
cursor: not-allowed;
box-shadow: $shadow-border;
}
}
}
}
.bottom {
height: 32px;
@media (max-width: $mobile-width) {
@ -454,6 +556,7 @@ export class BlockEditComponent implements AfterViewInit {
tag: '',
description: '',
is_done: true,
difficulty: 1,
});
// 1-based index of the centered card. 0/N+2 are placeholders.
@ -471,6 +574,7 @@ export class BlockEditComponent implements AfterViewInit {
tag: b.tag,
description: b.description,
is_done: b.is_done,
difficulty: b.difficulty ?? 1,
});
}
untracked(() => this.editedValues.set(m));
@ -510,6 +614,7 @@ export class BlockEditComponent implements AfterViewInit {
tag: '',
description: '',
is_done: false,
difficulty: 1,
}
);
}
@ -519,6 +624,10 @@ export class BlockEditComponent implements AfterViewInit {
return v.tag ? getColorOfTag(v.tag, this.baseColor()) : 'transparent';
}
tagPlaceholder(fallback: string): string {
return this.tags().length === 0 ? 'No tags yet. Open to create one.' : fallback;
}
colorOfNewTag = computed(() => {
const t = this.newValue().tag;
return t ? getColorOfTag(t, this.baseColor()) : 'transparent';
@ -561,6 +670,12 @@ export class BlockEditComponent implements AfterViewInit {
this.flushExisting(id);
}
updateDifficulty(id: string, delta: number): void {
const next = Math.max(1, this.editedFor(id).difficulty + delta);
this.patchEdited(id, { difficulty: next });
this.flushExisting(id);
}
private patchEdited(id: string, patch: Partial<EditedValue>): void {
this.editedValues.update((m) => {
const v = m.get(id);
@ -574,7 +689,13 @@ export class BlockEditComponent implements AfterViewInit {
flushExisting(id: string): void {
const v = this.editedFor(id);
if (!v.tag) return; // Skip empty saves
this.save.emit({ id, tag: v.tag, description: v.description, is_done: v.is_done });
this.save.emit({
id,
tag: v.tag,
description: v.description,
is_done: v.is_done,
difficulty: v.difficulty,
});
}
onDelete(id: string): void {
@ -595,6 +716,10 @@ export class BlockEditComponent implements AfterViewInit {
this.newValue.update((v) => ({ ...v, is_done }));
}
updateNewDifficulty(delta: number): void {
this.newValue.update((v) => ({ ...v, difficulty: Math.max(1, v.difficulty + delta) }));
}
submitNew(): void {
const v = this.newValue();
if (!v.tag) return;
@ -603,6 +728,7 @@ export class BlockEditComponent implements AfterViewInit {
tag: v.tag,
description: v.description,
is_done: v.is_done,
difficulty: v.difficulty,
});
this.close.emit();
}