This commit is contained in:
Andras Schmelczer 2026-05-31 09:39:34 +01:00
parent ad7968dadd
commit 5bf8e752e7
22 changed files with 81 additions and 112 deletions

View file

@ -8,7 +8,6 @@
],
"analytics": false
},
"newProjectRoot": "projects",
"projects": {
"frontend": {
"projectType": "application",

View file

@ -4,7 +4,6 @@ import {
input,
output,
signal,
computed,
effect,
viewChild,
ElementRef,
@ -614,10 +613,12 @@ export class BlockEditComponent implements AfterViewInit {
const t = this.tags();
untracked(() => {
const cur = this.newValue();
if (!cur.tag && t.length > 0) {
this.newValue.set({ ...cur, tag: t[0], is_done: this.defaultDone() });
} else if (!cur.tag) {
this.newValue.set({ ...cur, is_done: this.defaultDone() });
if (!cur.tag) {
this.newValue.set({
...cur,
tag: t.length > 0 ? t[0] : '',
is_done: this.defaultDone(),
});
}
});
});
@ -648,19 +649,21 @@ export class BlockEditComponent implements AfterViewInit {
);
}
private colorOfTag(tag: string): string {
return tag ? getColorOfTag(tag, this.baseColor()) : 'transparent';
}
colorOfTagForBlock(id: string): string {
const v = this.editedFor(id);
return v.tag ? getColorOfTag(v.tag, this.baseColor()) : 'transparent';
return this.colorOfTag(this.editedFor(id).tag);
}
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';
});
colorOfNewTag(): string {
return this.colorOfTag(this.newValue().tag);
}
formatDate(ts: number, compact = false): string {
const d = new Date(ts * 1000);

View file

@ -16,9 +16,6 @@ export interface UpdatePagePayload {
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({
selector: 'lt-settings',
standalone: true,
@ -255,10 +252,15 @@ export class SettingsComponent {
readonly hideCreateTowerButton = 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
readonly tokenInput = signal('');
readonly tokenInputTouched = signal(false);
readonly isValidToken = computed(() => UUIDV4_RE.test(this.tokenInput()));
readonly isValidToken = computed(() =>
SettingsComponent.UUIDV4_RE.test(this.tokenInput()),
);
constructor() {
effect(() => {

View file

@ -138,12 +138,14 @@ export class TowerSettingsComponent implements OnInit {
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).
if (this.form.invalid) return;
this.emitSave();
this.tryEmitSave();
}
/** Emit a save only when the form is valid (skips e.g. an empty name). */
private autoSave(): void {
this.tryEmitSave();
}
private tryEmitSave(): void {
if (this.form.invalid) return;
this.emitSave();
}

View file

@ -214,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');
}
}

View file

@ -71,12 +71,6 @@
}
}
p {
strong {
font-weight: bold;
}
}
.confirm-buttons {
display: flex;
justify-content: center;

View file

@ -38,9 +38,10 @@ export class PagesComponent implements OnDestroy {
constructor() {
effect(() => {
if (!this.store.loading() && this.store.pages().length === 0) {
const pages = this.store.pages();
if (!this.store.loading() && pages.length === 0) {
this.showWelcome.set(true);
} else if (this.store.pages().length > 0) {
} else if (pages.length > 0) {
this.showWelcome.set(false);
}
});
@ -89,9 +90,10 @@ export class PagesComponent implements OnDestroy {
});
readonly selectedPageIndex = computed(() => {
const pages = this.store.pages();
const page = this.selectedPage();
if (!page) return -1;
return this.store.pages().findIndex((p) => p.id === page.id);
return pages.findIndex((p) => p.id === page.id);
});
onSelectPage(index: number): void {

View file

@ -185,9 +185,8 @@ export class ColorPickerComponent {
return `hsl(${h}, 70%, 55%)`;
}
toCss(c: HslColor): string {
return toCss(c);
}
/** Re-exported so the template can call the utility directly. */
readonly toCss = toCss;
pickHue(h: number): void {
this.colorChange.emit({ h: h / 360, s: FIXED_S, l: FIXED_L });

View file

@ -18,7 +18,7 @@ export interface DoubleSliderRange<T> {
* Two-thumb range slider legacy "double-slider".
* Hands an indexed range over an arbitrary values array; emits the
* underlying values on each change. Labels magnetically lift as a thumb
* approaches them (rotated -45°), per the legacy.
* approaches them (rotated -30°), per the legacy.
*/
@Component({
selector: 'lt-double-slider',
@ -228,7 +228,7 @@ export class DoubleSliderComponent {
/**
* Magnetic label position: returns a CSS `transform` that lifts the label
* upward and rotates -45° as a thumb approaches.
* upward and rotates -30° as a thumb approaches.
*/
getOffset(index: number): string {
const labelIndex = index / Math.max(1, this.drawnLabels().length - 1);

View file

@ -695,6 +695,8 @@ export class TowerComponent implements AfterViewInit, OnDestroy {
if (this.isFirstRun && animateInitialStack && maxVisibleBlocks === null) {
this._visibleBlocks.set([]);
this.hiddenBlockCount.set(0);
this.prevDoneIds = allDone.map((b) => b.id);
this.isFirstRun = false;
return;
}
@ -708,9 +710,6 @@ export class TowerComponent implements AfterViewInit, OnDestroy {
newIds.length === 1 &&
prev.every((id) => ids.includes(id)); // no IDs disappeared
const inRange = (b: Block) =>
!range || (b.created_at >= range.from && b.created_at <= range.to);
const styled: StyledBlock[] = [];
for (const b of allDone) {
if (range && b.created_at < range.from) {
@ -755,7 +754,7 @@ export class TowerComponent implements AfterViewInit, OnDestroy {
this.hiddenBlockCount.set(hiddenCount);
if (this.isFirstRun && animateInitialStack) {
const initialBlocks = visibleStyled.filter((b) => b._opacity === '1' && inRange(b));
const initialBlocks = visibleStyled.filter((b) => b._opacity === '1');
if (initialBlocks.length > 0) {
this.startDescendAnimation(visibleStyled, initialBlocks);
this.prevDoneIds = ids;
@ -767,7 +766,7 @@ export class TowerComponent implements AfterViewInit, OnDestroy {
if (grewByOne) {
const newId = newIds[0];
const newBlock = visibleStyled.find((b) => b.id === newId);
if (newBlock && inRange(newBlock)) {
if (newBlock) {
// Snap newly-added in-range block to start position, then on the next
// paint flip it back to rest — that's what makes it visibly fall.
this.startDescendAnimation(visibleStyled, [newBlock]);

View file

@ -333,20 +333,20 @@ import { A11yModule } from '@angular/cdk/a11y';
min-width: 0;
}
.basic__label {
.basic__label,
.basic__text {
font-family: inherit;
font-weight: inherit;
color: $text-color;
font-size: inherit;
line-height: inherit;
}
.basic__label {
color: $text-color;
}
.basic__text {
font-family: inherit;
font-weight: inherit;
color: rgba($text-color, 0.82);
font-size: inherit;
line-height: inherit;
}
.actions {

View file

@ -21,23 +21,6 @@ describe('ApiService', () => {
http.verify();
});
it('gets health', async () => {
const promise = service.health();
const req = http.expectOne('/api/v1/health');
expect(req.request.method).toBe('GET');
req.flush({ status: 'ok' });
await expect(promise).resolves.toEqual({ status: 'ok' });
});
it('registers a token', async () => {
const promise = service.register('token-1');
const req = http.expectOne('/api/v1/register');
expect(req.request.method).toBe('POST');
expect(req.request.body).toEqual({ token: 'token-1' });
req.flush({ user_id: 'token-1' });
await expect(promise).resolves.toEqual({ user_id: 'token-1' });
});
it('gets data with a bearer token', async () => {
const tree: TreeDto = { pages: [] };
const promise = service.getData('token-1');

View file

@ -16,5 +16,5 @@ export function hash(s: string): number {
h = ((h << 5) - h + s.charCodeAt(i)) | 0;
}
// Map the signed int32 to [0, 1) — same formula as legacy
return h / (Math.pow(2, 32) - 2) + 0.5;
return h / (2 ** 32 - 2) + 0.5;
}

View file

@ -10,6 +10,6 @@
"src/**/*.ts"
],
"exclude": [
"src/**/*.spec.ts"
"src/**/*.vitest.ts"
]
}

View file

@ -10,6 +10,6 @@
},
"include": [
"src/**/*.d.ts",
"src/**/*.spec.ts"
"src/**/*.vitest.ts"
]
}

View file

@ -12,7 +12,7 @@ export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
include: ['src/**/*.vitest.ts', 'src/**/*.spec.vitest.ts'],
include: ['src/**/*.vitest.ts'],
setupFiles: ['./vitest.setup.ts'],
coverage: {
provider: 'v8',