From cbe66d75a5a90a478262afdbdd9f893058bc8284 Mon Sep 17 00:00:00 2001 From: Kristian Wind Date: Tue, 18 Aug 2026 21:45:31 +0200 Subject: [PATCH] =?UTF-8?q?feat(capture):=20skriv=20hvor=20som=20helst,=20?= =?UTF-8?q?og=20se=20hvad=20feltet=20forst=C3=A5r?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To ting fra Doda. Begynder man at skrive et sted i appen, åbner hurtig tilføjelse med bogstavet allerede i feltet — ingen knap at ramme først, og tanken skal ikke overleve en rejse hen for at finde et sted at ligge. Og under feltet står nu, hvad parseren kan læse: #projekt, /sektion, @etiket, p1, en dato. Det stod i pladsholderen før, som er det ene sted en hjælp ikke kan stå: den forsvinder ved første anslag, altså præcis når den begynder at være nyttig. Prisen er de enkelte bogstaver som genveje. Ét tastatur kan ikke både stave "tal med Anders" og springe til I dag på samme t, og det, man gør halvtreds gange om dagen, er at fange. Navigation ligger i paletten, som kan mere og koster ét anslag. Genvejssiden er rettet, så den ikke lover q, t og u. Undervejs, i den rækkefølge testene fandt dem: reglen var først "er det et tekstfelt?", hvilket stjal bogstaver fra en fokuseret knap eller liste — den er nu "har noget som helst fokus?". Hjælpelinjen lå i flowet og skubbede hele listen ned, når feltet fik fokus, så klik landede en række forkert; tolv tests fangede det. Den svæver nu, og efter at have svævet ovenpå den første opgave og slugt dens klik tager den ikke længere imod pegeredskaber. Og en fejl, der allerede lå der: q slog feltet op på aria-label="Ny opgave", så genvejen gjorde stille ingenting for enhver, der kørte fladen på engelsk. Feltet findes nu på et mærke, ikke på en oversat tekst. Co-Authored-By: Claude Opus 5 --- web/e2e/smoke.spec.js | 46 +++++++++++++++++++ web/src/lib/components/QuickAdd.svelte | 58 ++++++++++++++++++++++++ web/src/lib/locales/da.js | 8 ++++ web/src/lib/locales/en.js | 8 ++++ web/src/routes/+layout.svelte | 62 ++++++++++++++++---------- web/src/routes/genveje/+page.svelte | 16 ++++--- 6 files changed, 168 insertions(+), 30 deletions(-) diff --git a/web/e2e/smoke.spec.js b/web/e2e/smoke.spec.js index f6ed4f8..2abb197 100644 --- a/web/e2e/smoke.spec.js +++ b/web/e2e/smoke.spec.js @@ -1768,3 +1768,49 @@ test('MCP-adressen svarer JSON, ikke app-skallen', async ({ request }) => { expect(response.headers()['content-type']).toContain('application/json'); expect((await response.json()).code).toBe('unauthorized'); }); + +test('man kan skrive hvor som helst, og feltet fortæller hvad det forstår', async ({ page }) => { + const trouble = watchForTrouble(page); + await page.goto('/'); + + // Nothing focused, nothing clicked: just type. The thought does not have to + // survive a journey to find somewhere to put it. + await page.locator('body').click({ position: { x: 5, y: 5 } }); + // "i dag" because this is the Today view: a task parsed onto no date at all is + // correctly absent from it, which would read as the capture having failed. + await page.keyboard.type('ring til Anders i dag'); + + const field = page.locator('[data-quickadd]'); + await expect(field).toBeFocused(); + // Every letter arrives, including the first — which is the one a naive + // implementation drops while it is busy moving focus. + await expect(field).toHaveValue('ring til Anders i dag'); + + // And the legend appears with the field, saying what the parser can read. It + // used to live in the placeholder, which vanishes at the first keystroke — + // exactly when the help starts being useful. + await expect(page.getByText('projekt', { exact: false }).first()).toBeVisible(); + + await page.keyboard.press('Enter'); + await expect(page.getByText('ring til Anders', { exact: true })).toBeVisible(); + + // In English too. The shortcut used to find the field by its Danish label, so + // it silently did nothing for anybody running the interface in English. + await page.goto('/indstillinger'); + await page.getByLabel('Sprog').selectOption('en'); + await page.getByRole('button', { name: 'Gem' }).first().click(); + await page.goto('/'); + await page.locator('body').click({ position: { x: 5, y: 5 } }); + await page.keyboard.type('call Anders'); + await expect(page.locator('[data-quickadd]')).toHaveValue('call Anders'); + + // Back to Danish. The account is shared with every test after this one, and + // leaving it in English made three of them hunt for buttons by names that no + // longer existed — a failure that looks like the feature under test and is not. + await page.goto('/indstillinger'); + await page.getByLabel('Language').selectOption('da'); + await page.getByRole('button', { name: 'Save' }).first().click(); + await expect(page.getByRole('button', { name: 'Gem' }).first()).toBeVisible(); + + expect(trouble).toEqual([]); +}); diff --git a/web/src/lib/components/QuickAdd.svelte b/web/src/lib/components/QuickAdd.svelte index 74e76b6..402bb81 100644 --- a/web/src/lib/components/QuickAdd.svelte +++ b/web/src/lib/components/QuickAdd.svelte @@ -21,6 +21,19 @@ let spans = $state([]); let submitting = $state(false); let input; + let focused = $state(false); + + // Kept beside the parser's own vocabulary on purpose: a legend that promises + // something internal/quickadd cannot read is worse than no legend. These four + // are what Parse recognises as marks; dates it reads as prose, which is why the + // last entry is an example rather than a symbol. + let syntax = $derived([ + { mark: '#', what: t('task.syntaxProject') }, + { mark: '/', what: t('task.syntaxSection') }, + { mark: '@', what: t('task.syntaxLabel') }, + { mark: 'p1', what: t('task.syntaxPriority') }, + { mark: t('task.syntaxDateMark'), what: t('task.syntaxDate') } + ]); let previewController = null; let previewTimer = null; @@ -143,7 +156,10 @@ bind:this={input} bind:value={text} {onkeydown} + onfocus={() => (focused = true)} + onblur={() => (focused = false)} type="text" + data-quickadd placeholder={t('task.placeholder')} aria-label={t('task.new')} autocomplete="off" @@ -154,10 +170,52 @@ {#if text.trim()} {/if} + + + {#if focused} +

+ {#each syntax as item} + {item.mark} {item.what} + {/each} +

+ {/if}