This commit is contained in:
Andras Schmelczer 2026-05-29 08:12:35 +01:00
parent f74ee43cb4
commit e2a60e71a3
30 changed files with 585 additions and 33 deletions

View file

@ -7,8 +7,15 @@ import { test } from '@playwright/test';
test.describe('Life Towers visuals', () => {
test('capture key UI states', async ({ page }) => {
await page.goto('/');
await page.waitForSelector('text=Add a new page to get started!', { timeout: 15000 });
await page.screenshot({ path: 'visuals/01-empty-state.png', fullPage: true });
await page.waitForSelector('text=Welcome to Life Towers', { timeout: 15000 });
await page.waitForTimeout(350); // let the welcome modal finish fade-in
await page.screenshot({ path: 'visuals/01-welcome-modal.png', fullPage: true });
// Dismiss the welcome modal with Start fresh, then continue.
await page.getByRole('button', { name: 'Start fresh' }).click();
await page.waitForSelector('section.modal', { state: 'detached' });
await page.screenshot({ path: 'visuals/01b-empty-state-after-dismiss.png', fullPage: true });
// Open the page dropdown (without creating a page yet).
await page.locator('lt-select-add .top').first().click();
@ -26,7 +33,7 @@ test.describe('Life Towers visuals', () => {
await page.locator('img[alt="Add tower"]').click();
await page.waitForSelector('section.modal.active');
await page.waitForTimeout(350);
await page.locator('input[placeholder="Tower name…"]').fill('Reading');
await page.locator('input[placeholder="New tower"]').fill('Reading');
await page.screenshot({ path: 'visuals/03-new-tower-modal.png', fullPage: true });
await page.locator('lt-tower-settings button[type="submit"]').click();
await page.waitForSelector('section.modal', { state: 'detached' });
@ -100,7 +107,7 @@ test.describe('Life Towers visuals', () => {
await page.locator('img[alt="Add tower"]').click();
await page.waitForSelector('section.modal.active');
await page.waitForTimeout(350);
await page.locator('input[placeholder="Tower name…"]').fill('Side projects');
await page.locator('input[placeholder="New tower"]').fill('Side projects');
await page.locator('lt-tower-settings button[type="submit"]').click();
await page.waitForSelector('section.modal', { state: 'detached' });
await page.waitForTimeout(300);
@ -157,4 +164,39 @@ test.describe('Life Towers visuals', () => {
await page.waitForTimeout(350);
await page.screenshot({ path: 'visuals/11-settings-modal.png', fullPage: true });
});
test('"Try an example" populates a sample page', async ({ page }) => {
await page.goto('/');
await page.waitForSelector('text=Welcome to Life Towers', { timeout: 15000 });
await page.waitForTimeout(350);
await page.getByRole('button', { name: 'Try an example' }).click();
await page.waitForSelector('section.modal', { state: 'detached' });
await page.waitForTimeout(1800);
await page.screenshot({ path: 'visuals/12-example-data.png', fullPage: true });
});
test('Mobile viewport — welcome + example + carousel', async ({ browser }) => {
const ctx = await browser.newContext({ viewport: { width: 390, height: 844 } });
const page = await ctx.newPage();
await page.goto((process.env['PLAYWRIGHT_BASE_URL'] ?? 'http://localhost:8000') + '/');
await page.waitForSelector('text=Welcome to Life Towers', { timeout: 15000 });
await page.waitForTimeout(350);
await page.screenshot({ path: 'visuals/13-mobile-welcome.png', fullPage: true });
await page.getByRole('button', { name: 'Try an example' }).click();
await page.waitForSelector('section.modal', { state: 'detached' });
await page.waitForTimeout(1800);
await page.screenshot({ path: 'visuals/14-mobile-populated.png', fullPage: true });
// Open the block-edit carousel for the first tower's first task.
await page.locator('lt-tasks .container').first().click();
await page.waitForTimeout(400);
await page.locator('lt-tasks .task-container').first().click();
await page.waitForSelector('section.modal.active');
await page.waitForTimeout(400);
await page.screenshot({ path: 'visuals/15-mobile-carousel.png', fullPage: true });
await ctx.close();
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before After
Before After

View file

@ -0,0 +1,13 @@
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><defs>
<linearGradient id="bg" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fffcf0"/><stop offset="1" stop-color="#ffebeb"/></linearGradient>
<linearGradient id="b1" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#9a6069"/><stop offset="1" stop-color="#7f4f59"/></linearGradient>
<linearGradient id="b2" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#b07480"/><stop offset="1" stop-color="#9a6069"/></linearGradient>
<linearGradient id="b3" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#c98579"/><stop offset="1" stop-color="#b87672"/></linearGradient>
<linearGradient id="b4" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e2987f"/><stop offset="1" stop-color="#d4836f"/></linearGradient>
<filter id="sh" x="-30%" y="-30%" width="160%" height="190%"><feDropShadow dx="0" dy="6" stdDeviation="7" flood-color="#5d576b" flood-opacity="0.18"/></filter>
</defs><rect width="512" height="512" rx="112" fill="url(#bg)"/><g filter="url(#sh)">
<rect x="134" y="360" width="244" height="64" rx="18" fill="url(#b1)"/>
<rect x="148" y="286" width="216" height="64" rx="18" fill="url(#b2)"/>
<rect x="162" y="212" width="188" height="64" rx="18" fill="url(#b3)"/>
<g transform="rotate(-7 256 158)"><rect x="176" y="126" width="160" height="64" rx="18" fill="url(#b4)"/></g>
</g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

View file

@ -0,0 +1,13 @@
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><defs>
<linearGradient id="bg" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fffcf0"/><stop offset="1" stop-color="#ffebeb"/></linearGradient>
<linearGradient id="b1" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#9a6069"/><stop offset="1" stop-color="#7f4f59"/></linearGradient>
<linearGradient id="b2" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#b07480"/><stop offset="1" stop-color="#9a6069"/></linearGradient>
<linearGradient id="b3" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#c98579"/><stop offset="1" stop-color="#b87672"/></linearGradient>
<linearGradient id="b4" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e2987f"/><stop offset="1" stop-color="#d4836f"/></linearGradient>
<filter id="sh" x="-30%" y="-30%" width="160%" height="190%"><feDropShadow dx="0" dy="6" stdDeviation="7" flood-color="#5d576b" flood-opacity="0.18"/></filter>
</defs><rect width="512" height="512" rx="112" fill="url(#bg)"/><g filter="url(#sh)">
<rect x="134" y="360" width="244" height="64" rx="18" fill="url(#b1)"/>
<rect x="148" y="286" width="216" height="64" rx="18" fill="url(#b2)"/>
<rect x="162" y="212" width="188" height="64" rx="18" fill="url(#b3)"/>
<g transform="rotate(-7 256 158)"><rect x="176" y="126" width="160" height="64" rx="18" fill="url(#b4)"/></g>
</g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,13 @@
<svg width="1118" height="200" viewBox="0 0 1118 200" xmlns="http://www.w3.org/2000/svg"><defs>
<linearGradient id="bg" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fffcf0"/><stop offset="1" stop-color="#ffebeb"/></linearGradient>
<linearGradient id="b1" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#9a6069"/><stop offset="1" stop-color="#7f4f59"/></linearGradient>
<linearGradient id="b2" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#b07480"/><stop offset="1" stop-color="#9a6069"/></linearGradient>
<linearGradient id="b3" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#c98579"/><stop offset="1" stop-color="#b87672"/></linearGradient>
<linearGradient id="b4" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e2987f"/><stop offset="1" stop-color="#d4836f"/></linearGradient>
<filter id="sh" x="-30%" y="-30%" width="160%" height="190%"><feDropShadow dx="0" dy="6" stdDeviation="7" flood-color="#5d576b" flood-opacity="0.18"/></filter>
</defs><g transform="scale(0.39063)"><rect width="512" height="512" rx="112" fill="url(#bg)"/><g filter="url(#sh)">
<rect x="134" y="360" width="244" height="64" rx="18" fill="url(#b1)"/>
<rect x="148" y="286" width="216" height="64" rx="18" fill="url(#b2)"/>
<rect x="162" y="212" width="188" height="64" rx="18" fill="url(#b3)"/>
<g transform="rotate(-7 256 158)"><rect x="176" y="126" width="160" height="64" rx="18" fill="url(#b4)"/></g>
</g></g><g transform="translate(229.76 158.40)"><path d="M91.04 0L14.24 0L14.24-113.60L25.44-113.60L25.44-9.92L91.04-9.92L91.04 0M116.16 0L105.28 0L105.28-83.36L116.16-83.36L116.16 0M116.16-100.80L105.28-100.80L105.28-116.80L116.16-116.80L116.16-100.80M155.04 0L144.16 0L144.16-74.72L132.64-74.72L132.64-83.36L144.16-83.36L144.16-85.60Q144.16-95.84 147.12-103.20Q150.08-110.56 155.60-114.48Q161.12-118.40 168.64-118.40Q173.60-118.40 178.24-116.96Q182.88-115.52 186.24-113.12L182.88-105.28Q180.80-107.04 177.44-108Q174.08-108.96 170.72-108.96Q163.20-108.96 159.12-103.04Q155.04-97.12 155.04-85.92L155.04-83.36L178.08-83.36L178.08-74.72L155.04-74.72L155.04 0M228.96 1.60Q220 1.60 212.32-1.84Q204.64-5.28 199.04-11.28Q193.44-17.28 190.32-25.12Q187.20-32.96 187.20-41.92Q187.20-53.60 192.56-63.36Q197.92-73.12 207.36-78.96Q216.80-84.80 228.80-84.80Q241.12-84.80 250.32-78.88Q259.52-72.96 264.80-63.28Q270.08-53.60 270.08-42.08Q270.08-40.80 270.08-39.60Q270.08-38.40 269.92-37.76L198.56-37.76Q199.36-28.80 203.60-21.84Q207.84-14.88 214.64-10.80Q221.44-6.72 229.44-6.72Q237.60-6.72 244.88-10.88Q252.16-15.04 255.04-21.76L264.48-19.20Q261.92-13.28 256.64-8.48Q251.36-3.68 244.24-1.04Q237.12 1.60 228.96 1.60M198.24-45.60L259.84-45.60Q259.20-54.72 254.96-61.60Q250.72-68.48 243.92-72.40Q237.12-76.32 228.96-76.32Q220.80-76.32 214.08-72.40Q207.36-68.48 203.12-61.52Q198.88-54.56 198.24-45.60M410.56-113.60L410.56-103.68L370.08-103.68L370.08 0L358.88 0L358.88-103.68L318.40-103.68L318.40-113.60L410.56-113.60M460.80 1.60Q451.84 1.60 444.24-1.84Q436.64-5.28 431.12-11.28Q425.60-17.28 422.56-25.04Q419.52-32.80 419.52-41.44Q419.52-50.40 422.56-58.16Q425.60-65.92 431.20-71.92Q436.80-77.92 444.40-81.36Q452-84.80 460.96-84.80Q469.92-84.80 477.44-81.36Q484.96-77.92 490.56-71.92Q496.16-65.92 499.20-58.16Q502.24-50.40 502.24-41.44Q502.24-32.80 499.20-25.04Q496.16-17.28 490.64-11.28Q485.12-5.28 477.52-1.84Q469.92 1.60 460.80 1.60M430.56-41.28Q430.56-32 434.64-24.40Q438.72-16.80 445.60-12.40Q452.48-8 460.80-8Q469.12-8 476-12.48Q482.88-16.96 487.04-24.72Q491.20-32.48 491.20-41.60Q491.20-50.88 487.04-58.56Q482.88-66.24 476-70.72Q469.12-75.20 460.80-75.20Q452.48-75.20 445.68-70.56Q438.88-65.92 434.72-58.32Q430.56-50.72 430.56-41.28M595.20-11.20L625.44-83.36L636.16-83.36L600.32 0L590.88 0L573.44-41.44L556.16 0L546.72 0L510.88-83.36L521.44-83.36L551.84-11.20L567.20-48.80L553.12-83.20L562.88-83.20L573.44-56.16L584.16-83.20L593.76-83.20L579.84-48.80L595.20-11.20M686.56 1.60Q677.60 1.60 669.92-1.84Q662.24-5.28 656.64-11.28Q651.04-17.28 647.92-25.12Q644.80-32.96 644.80-41.92Q644.80-53.60 650.16-63.36Q655.52-73.12 664.96-78.96Q674.40-84.80 686.40-84.80Q698.72-84.80 707.92-78.88Q717.12-72.96 722.40-63.28Q727.68-53.60 727.68-42.08Q727.68-40.80 727.68-39.60Q727.68-38.40 727.52-37.76L656.16-37.76Q656.96-28.80 661.20-21.84Q665.44-14.88 672.24-10.80Q679.04-6.72 687.04-6.72Q695.20-6.72 702.48-10.88Q709.76-15.04 712.64-21.76L722.08-19.20Q719.52-13.28 714.24-8.48Q708.96-3.68 701.84-1.04Q694.72 1.60 686.56 1.60M655.84-45.60L717.44-45.60Q716.80-54.72 712.56-61.60Q708.32-68.48 701.52-72.40Q694.72-76.32 686.56-76.32Q678.40-76.32 671.68-72.40Q664.96-68.48 660.72-61.52Q656.48-54.56 655.84-45.60M786.08-83.68L786.08-73.76Q775.20-73.44 766.96-67.68Q758.72-61.92 755.36-51.84L755.36 0L744.48 0L744.48-83.36L754.72-83.36L754.72-63.36Q759.04-72.16 766.16-77.60Q773.28-83.04 781.28-83.68Q782.88-83.84 784.08-83.84Q785.28-83.84 786.08-83.68M828 1.60Q817.76 1.60 808.96-1.76Q800.16-5.12 793.76-12L798.24-19.68Q805.28-13.12 812.40-10.16Q819.52-7.20 827.52-7.20Q837.28-7.20 843.36-11.12Q849.44-15.04 849.44-22.40Q849.44-27.36 846.48-30Q843.52-32.64 838-34.32Q832.48-36 824.80-37.92Q816.16-40.32 810.32-42.96Q804.48-45.60 801.52-49.68Q798.56-53.76 798.56-60.32Q798.56-68.48 802.64-73.84Q806.72-79.20 813.84-82Q820.96-84.80 829.76-84.80Q839.36-84.80 846.72-81.76Q854.08-78.72 858.72-73.28L853.44-65.92Q848.96-71.04 842.80-73.52Q836.64-76 829.12-76Q824-76 819.36-74.64Q814.72-73.28 811.76-70.16Q808.80-67.04 808.80-61.60Q808.80-57.12 811.04-54.64Q813.28-52.16 817.76-50.48Q822.24-48.80 828.80-46.88Q838.24-44.32 845.28-41.68Q852.32-39.04 856.16-34.88Q860-30.72 860-23.20Q860-11.52 851.20-4.96Q842.40 1.60 828 1.60" fill="#5d576b"/></g></svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

13
frontend/public/logo.svg Normal file
View file

@ -0,0 +1,13 @@
<svg width="1118" height="200" viewBox="0 0 1118 200" xmlns="http://www.w3.org/2000/svg"><defs>
<linearGradient id="bg" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#fffcf0"/><stop offset="1" stop-color="#ffebeb"/></linearGradient>
<linearGradient id="b1" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#9a6069"/><stop offset="1" stop-color="#7f4f59"/></linearGradient>
<linearGradient id="b2" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#b07480"/><stop offset="1" stop-color="#9a6069"/></linearGradient>
<linearGradient id="b3" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#c98579"/><stop offset="1" stop-color="#b87672"/></linearGradient>
<linearGradient id="b4" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#e2987f"/><stop offset="1" stop-color="#d4836f"/></linearGradient>
<filter id="sh" x="-30%" y="-30%" width="160%" height="190%"><feDropShadow dx="0" dy="6" stdDeviation="7" flood-color="#5d576b" flood-opacity="0.18"/></filter>
</defs><g transform="scale(0.39063)"><rect width="512" height="512" rx="112" fill="url(#bg)"/><g filter="url(#sh)">
<rect x="134" y="360" width="244" height="64" rx="18" fill="url(#b1)"/>
<rect x="148" y="286" width="216" height="64" rx="18" fill="url(#b2)"/>
<rect x="162" y="212" width="188" height="64" rx="18" fill="url(#b3)"/>
<g transform="rotate(-7 256 158)"><rect x="176" y="126" width="160" height="64" rx="18" fill="url(#b4)"/></g>
</g></g><g transform="translate(229.76 158.40)"><path d="M91.04 0L14.24 0L14.24-113.60L25.44-113.60L25.44-9.92L91.04-9.92L91.04 0M116.16 0L105.28 0L105.28-83.36L116.16-83.36L116.16 0M116.16-100.80L105.28-100.80L105.28-116.80L116.16-116.80L116.16-100.80M155.04 0L144.16 0L144.16-74.72L132.64-74.72L132.64-83.36L144.16-83.36L144.16-85.60Q144.16-95.84 147.12-103.20Q150.08-110.56 155.60-114.48Q161.12-118.40 168.64-118.40Q173.60-118.40 178.24-116.96Q182.88-115.52 186.24-113.12L182.88-105.28Q180.80-107.04 177.44-108Q174.08-108.96 170.72-108.96Q163.20-108.96 159.12-103.04Q155.04-97.12 155.04-85.92L155.04-83.36L178.08-83.36L178.08-74.72L155.04-74.72L155.04 0M228.96 1.60Q220 1.60 212.32-1.84Q204.64-5.28 199.04-11.28Q193.44-17.28 190.32-25.12Q187.20-32.96 187.20-41.92Q187.20-53.60 192.56-63.36Q197.92-73.12 207.36-78.96Q216.80-84.80 228.80-84.80Q241.12-84.80 250.32-78.88Q259.52-72.96 264.80-63.28Q270.08-53.60 270.08-42.08Q270.08-40.80 270.08-39.60Q270.08-38.40 269.92-37.76L198.56-37.76Q199.36-28.80 203.60-21.84Q207.84-14.88 214.64-10.80Q221.44-6.72 229.44-6.72Q237.60-6.72 244.88-10.88Q252.16-15.04 255.04-21.76L264.48-19.20Q261.92-13.28 256.64-8.48Q251.36-3.68 244.24-1.04Q237.12 1.60 228.96 1.60M198.24-45.60L259.84-45.60Q259.20-54.72 254.96-61.60Q250.72-68.48 243.92-72.40Q237.12-76.32 228.96-76.32Q220.80-76.32 214.08-72.40Q207.36-68.48 203.12-61.52Q198.88-54.56 198.24-45.60" fill="#5d576b"/><path d="M410.56-113.60L410.56-103.68L370.08-103.68L370.08 0L358.88 0L358.88-103.68L318.40-103.68L318.40-113.60L410.56-113.60M460.80 1.60Q451.84 1.60 444.24-1.84Q436.64-5.28 431.12-11.28Q425.60-17.28 422.56-25.04Q419.52-32.80 419.52-41.44Q419.52-50.40 422.56-58.16Q425.60-65.92 431.20-71.92Q436.80-77.92 444.40-81.36Q452-84.80 460.96-84.80Q469.92-84.80 477.44-81.36Q484.96-77.92 490.56-71.92Q496.16-65.92 499.20-58.16Q502.24-50.40 502.24-41.44Q502.24-32.80 499.20-25.04Q496.16-17.28 490.64-11.28Q485.12-5.28 477.52-1.84Q469.92 1.60 460.80 1.60M430.56-41.28Q430.56-32 434.64-24.40Q438.72-16.80 445.60-12.40Q452.48-8 460.80-8Q469.12-8 476-12.48Q482.88-16.96 487.04-24.72Q491.20-32.48 491.20-41.60Q491.20-50.88 487.04-58.56Q482.88-66.24 476-70.72Q469.12-75.20 460.80-75.20Q452.48-75.20 445.68-70.56Q438.88-65.92 434.72-58.32Q430.56-50.72 430.56-41.28M595.20-11.20L625.44-83.36L636.16-83.36L600.32 0L590.88 0L573.44-41.44L556.16 0L546.72 0L510.88-83.36L521.44-83.36L551.84-11.20L567.20-48.80L553.12-83.20L562.88-83.20L573.44-56.16L584.16-83.20L593.76-83.20L579.84-48.80L595.20-11.20M686.56 1.60Q677.60 1.60 669.92-1.84Q662.24-5.28 656.64-11.28Q651.04-17.28 647.92-25.12Q644.80-32.96 644.80-41.92Q644.80-53.60 650.16-63.36Q655.52-73.12 664.96-78.96Q674.40-84.80 686.40-84.80Q698.72-84.80 707.92-78.88Q717.12-72.96 722.40-63.28Q727.68-53.60 727.68-42.08Q727.68-40.80 727.68-39.60Q727.68-38.40 727.52-37.76L656.16-37.76Q656.96-28.80 661.20-21.84Q665.44-14.88 672.24-10.80Q679.04-6.72 687.04-6.72Q695.20-6.72 702.48-10.88Q709.76-15.04 712.64-21.76L722.08-19.20Q719.52-13.28 714.24-8.48Q708.96-3.68 701.84-1.04Q694.72 1.60 686.56 1.60M655.84-45.60L717.44-45.60Q716.80-54.72 712.56-61.60Q708.32-68.48 701.52-72.40Q694.72-76.32 686.56-76.32Q678.40-76.32 671.68-72.40Q664.96-68.48 660.72-61.52Q656.48-54.56 655.84-45.60M786.08-83.68L786.08-73.76Q775.20-73.44 766.96-67.68Q758.72-61.92 755.36-51.84L755.36 0L744.48 0L744.48-83.36L754.72-83.36L754.72-63.36Q759.04-72.16 766.16-77.60Q773.28-83.04 781.28-83.68Q782.88-83.84 784.08-83.84Q785.28-83.84 786.08-83.68M828 1.60Q817.76 1.60 808.96-1.76Q800.16-5.12 793.76-12L798.24-19.68Q805.28-13.12 812.40-10.16Q819.52-7.20 827.52-7.20Q837.28-7.20 843.36-11.12Q849.44-15.04 849.44-22.40Q849.44-27.36 846.48-30Q843.52-32.64 838-34.32Q832.48-36 824.80-37.92Q816.16-40.32 810.32-42.96Q804.48-45.60 801.52-49.68Q798.56-53.76 798.56-60.32Q798.56-68.48 802.64-73.84Q806.72-79.20 813.84-82Q820.96-84.80 829.76-84.80Q839.36-84.80 846.72-81.76Q854.08-78.72 858.72-73.28L853.44-65.92Q848.96-71.04 842.80-73.52Q836.64-76 829.12-76Q824-76 819.36-74.64Q814.72-73.28 811.76-70.16Q808.80-67.04 808.80-61.60Q808.80-57.12 811.04-54.64Q813.28-52.16 817.76-50.48Q822.24-48.80 828.80-46.88Q838.24-44.32 845.28-41.68Q852.32-39.04 856.16-34.88Q860-30.72 860-23.20Q860-11.52 851.20-4.96Q842.40 1.60 828 1.60" fill="#a2666f"/></g></svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

View file

@ -27,7 +27,11 @@ import { getColorOfTag } from '../../utils/color';
position: absolute;
width: 100%;
height: 100%;
@include gravitate();
cursor: pointer;
@media (hover: hover) and (pointer: fine) {
@include gravitate();
}
}
}
`,

View file

@ -177,6 +177,11 @@ interface EditedValue {
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
scroll-snap-type: x mandatory;
@media (max-width: $mobile-width) {
padding: 0 7.5vw;
}
&::-webkit-scrollbar {
width: 0;
@ -192,8 +197,12 @@ interface EditedValue {
flex: 0 0 auto;
width: 66vw;
max-width: 400px;
scroll-snap-align: center;
@media (max-width: $mobile-width) {
width: 300px;
width: 85vw;
max-width: 85vw;
padding: var(--medium-padding);
margin: calc(var(--medium-padding) / 2);
opacity: 1 !important;
}
box-sizing: border-box;
@ -246,6 +255,10 @@ interface EditedValue {
opacity: 0 !important;
width: 60vw;
max-width: 60vw;
@media (max-width: $mobile-width) {
width: 7.5vw;
max-width: 7.5vw;
}
box-shadow: none;
background: transparent;
}
@ -256,7 +269,7 @@ interface EditedValue {
.exit {
position: absolute;
left: 0;
right: 0;
@include exit();
}
@ -291,6 +304,13 @@ interface EditedValue {
transform: translateY(-50%) translateX(-50%);
}
}
@media (max-width: $mobile-width) {
lt-select-add, lt-toggle {
max-width: 100%;
width: 100%;
}
}
}
`,
})
@ -389,10 +409,13 @@ export class BlockEditComponent implements AfterViewInit {
formatDate(ts: number): string {
const d = new Date(ts * 1000);
return d.toLocaleDateString(undefined, {
// e.g. "May 28, 2026, 14:32"
return d.toLocaleString(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
});
}

View file

@ -31,6 +31,14 @@ import { ModalStateService } from '../../services/modal-state.service';
styles: `
@import '../../../library/main';
/* lt-modal host must not occupy a flex slot in its parent section.modal
is position:fixed, but the host element itself would otherwise take a
slot and push siblings around when it mounts/unmounts.
display: contents removes the host box without affecting descendants. */
:host {
display: contents;
}
section.modal {
position: fixed;
top: 0;

View file

@ -111,7 +111,11 @@ const UUIDV4_RE =
@include card();
width: 66vw;
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;
padding: var(--large-padding);
position: relative;
@ -171,6 +175,13 @@ const UUIDV4_RE =
button {
margin: 0;
flex: 0 0 auto;
max-width: 100%;
}
@media (max-width: $mobile-width) {
flex-wrap: wrap;
input { width: 100%; }
button { margin-left: auto; }
}
}

View file

@ -21,10 +21,7 @@ export interface TowerSettingsResult {
imports: [ReactiveFormsModule, ColorPickerComponent],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="header">
<div class="exit" (click)="close.emit()" role="button" aria-label="Close"></div>
<h2>{{ tower() ? 'Tower settings' : 'New tower' }}</h2>
</div>
<div class="exit" (click)="close.emit()" role="button" aria-label="Close"></div>
<form [formGroup]="form" (ngSubmit)="onSubmit()">
<input
@ -32,12 +29,15 @@ export interface TowerSettingsResult {
name="towerName"
type="text"
formControlName="name"
placeholder="Tower name…"
[placeholder]="tower() ? 'Tower name…' : 'New tower'"
maxlength="200"
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)" />
</div>
<button type="submit" [disabled]="form.invalid">
{{ tower() ? 'Save' : 'Create tower' }}
@ -55,26 +55,41 @@ 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;
}
// Generous gap between the name input and the color picker — the picker
// is a substantial control and crowding it against the title looks busy.
.picker-row {
padding-top: var(--medium-padding);
}
button {

View file

@ -59,7 +59,9 @@
&:not(:nth-last-child(1)) {
margin-right: var(--medium-padding);
@media (max-width: $mobile-width) {
margin-right: var(--small-padding);
// 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);
}
}
}
@ -76,6 +78,35 @@
}
}
}
// 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) {
overflow-x: auto;
overflow-y: visible;
-webkit-overflow-scrolling: touch;
scroll-snap-type: x mandatory;
flex-wrap: nowrap;
justify-content: flex-start;
// Side padding lets the first and last tower scroll fully into view.
padding: 0 var(--medium-padding);
max-width: 100%;
&::-webkit-scrollbar {
display: none;
}
// Override the @for width-calc rules above.
& > * {
width: calc(66vw - var(--small-padding)) !important;
max-width: calc(66vw - var(--small-padding)) !important;
min-width: calc(66vw - var(--small-padding)) !important;
scroll-snap-align: start;
flex-shrink: 0;
}
}
}
.double-slider-container {
@ -89,7 +120,11 @@
@include card();
width: 66vw;
max-width: 500px;
@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);
position: relative;
@ -101,7 +136,7 @@
@include center-child();
.exit {
position: absolute;
left: var(--large-padding);
right: var(--large-padding);
@include exit();
}
}
@ -115,11 +150,21 @@
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

@ -11,14 +11,23 @@
.select-add-container {
width: 250px;
margin-left: auto;
margin-right: auto;
margin: 0 auto;
position: relative;
z-index: 1000;
@media (max-width: $mobile-width) {
width: 80vw;
max-width: 320px;
}
}
.page-container {
flex: 1 0 auto;
// Generous breathing room between the page selector dropdown and the
// towers the dropdown can open downward without crowding the towers.
padding-top: var(--large-padding);
@media (max-width: $mobile-width) {
padding-top: var(--medium-padding);
}
}
button {
@ -27,5 +36,10 @@
&.transparent {
opacity: 0;
}
@media (max-width: $mobile-width) {
margin-top: var(--medium-padding);
font-size: var(--medium-font-size);
}
}
}

View file

@ -65,6 +65,11 @@ export interface DoubleSliderRange<T> {
position: relative;
margin: calc(#{$slider-size} / 2) auto 0 auto;
@media (max-width: $mobile-width) {
max-width: 90vw;
margin-top: calc(#{$slider-size} / 2);
}
label { display: none; }
input[type='range'] {
@ -145,6 +150,12 @@ export interface DoubleSliderRange<T> {
transition: transform $long-animation-time;
white-space: nowrap;
}
@media (max-width: $mobile-width) {
font-size: var(--small-font-size);
margin-top: $slider-size;
span { margin-top: 10px; }
}
}
}
`,

View file

@ -92,6 +92,10 @@ import { getColorOfTag } from '../../utils/color';
align-items: center;
gap: var(--small-padding);
@media (max-width: $mobile-width) {
gap: calc(var(--small-padding) / 2);
}
&:hover p {
@media (min-width: $mobile-width) {
color: inherit !important;

View file

@ -166,6 +166,11 @@ interface StyledBlock extends Block {
@include inner-spacing(var(--medium-padding));
@media (max-width: $mobile-width) {
@include inner-spacing(var(--small-padding));
padding: var(--small-padding);
}
width: 100%;
:before {
@ -191,6 +196,11 @@ interface StyledBlock extends Block {
display: block;
width: 100%;
@media (max-width: $mobile-width) {
min-height: 44px;
max-height: 25vh;
}
.container {
max-height: 100%;
overflow-y: auto;
@ -261,6 +271,10 @@ interface StyledBlock extends Block {
@media (min-width: $mobile-width) {
width: 50%;
}
@media (max-width: $mobile-width) {
width: 100%;
}
}
}
}

View file

@ -38,7 +38,11 @@ import { Component, ChangeDetectionStrategy, output } from '@angular/core';
@include card();
width: 66vw;
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;
padding: var(--large-padding);
position: relative;
@ -72,6 +76,10 @@ import { Component, ChangeDetectionStrategy, output } from '@angular/core';
gap: var(--large-padding);
margin-top: var(--large-padding);
button {
max-width: 100%;
}
button.primary {
color: $accent-color;
border-bottom-color: rgba($accent-color, 0.33);

View file

@ -6,8 +6,18 @@
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<meta name="theme-color" content="#5d576b" />
<meta name="description" content="Organise your tasks into visual towers of blocks, grouped on pages." />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="apple-touch-icon" href="apple-touch-icon.png" />
<link rel="manifest" href="manifest.webmanifest" />
<meta property="og:title" content="Life Towers" />
<meta property="og:description" content="Organise your tasks into visual towers of blocks." />
<meta property="og:type" content="website" />
<meta property="og:image" content="og-image.png" />
<meta name="twitter:card" content="summary_large_image" />
</head>
<body>
<app-root></app-root>