diff --git a/AGENTS.md b/AGENTS.md index 0c6d6c12..981cc176 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,18 +52,20 @@ When introducing shortcuts, known issues, or deferred security work, file an iss ### CSS & Styling -- **All UI must work in both light and dark mode automatically.** Never use hard-coded colours (e.g., `text-gray-700`, `bg-white`, `border-slate-200`). Always use DaisyUI semantic colour classes (`text-base-content`, `bg-base-100`, `bg-base-200`, `border-base-300`, `text-primary`, `bg-error`, etc.) and Tailwind opacity modifiers on those classes (`text-base-content/60`, `bg-primary/10`) so that colours adapt to the active theme. +- **All UI must work in both light and dark mode automatically.** Never use hard-coded colours (e.g., `text-gray-700`, `bg-white`, `border-slate-200`). Always use DaisyUI semantic colour classes (`text-base-content`, `bg-base-100`, `bg-base-200`, `border-base-300`, `text-primary`, `bg-error`, etc.) and Tailwind opacity modifiers on those classes (`text-base-content/80`, `bg-primary/10`) so that colours adapt to the active theme. +- **Secondary text uses `text-muted`** (defined in `src/app.css`), never `text-base-content` at 60% opacity or less, and never `text-muted` with an opacity: those fail the 4.5:1 contrast ratio in the light theme. `aria-hidden` is no exemption, sighted users still read hidden text. Lower opacities are only allowed on `aria-hidden` icon components that duplicate adjacent text; icons that show a state (e.g. an expand chevron) need 3:1 and also use `text-muted` (enforced by `npm run test:arch`). - **All pages and components must be mobile-friendly.** Use responsive Tailwind breakpoints (`sm:`, `md:`, `lg:`) and test layouts at mobile viewport widths. Navigation must be accessible on small screens. - Use Tailwind v4 `@source` directive for component CSS scanning -### Accessibility (BITV 2.0) +### Accessibility (WCAG 2.2 AA) -The application must comply with **BITV 2.0** (German accessibility regulation, based on WCAG 2.1 AA). Key requirements: +The application must comply with **WCAG 2.2 AA**. This covers **BITV 2.0** (German accessibility regulation, based on WCAG 2.1 AA). The axe tests in `e2e/accessibility.spec.ts` check the WCAG 2.0, 2.1 and 2.2 A/AA rules. Key requirements: - **Semantic HTML**: Use correct elements (` {/if} diff --git a/src/routes/(app)/storage/+layout.svelte b/src/routes/(app)/storage/+layout.svelte index ef0849c3..34b45ac7 100644 --- a/src/routes/(app)/storage/+layout.svelte +++ b/src/routes/(app)/storage/+layout.svelte @@ -42,7 +42,9 @@ void goto( (resolve as (pathname: import('$app/types').Pathname) => string)( locationPath(connection, bucket, prefix) as import('$app/types').Pathname - ) + ), + // Keep focus on the tab that triggered the switch instead of resetting it to + { keepFocus: true } ); }, replaceLocationUrl: (connection, bucket, prefix) => { @@ -50,7 +52,7 @@ (resolve as (pathname: import('$app/types').Pathname) => string)( locationPath(connection, bucket, prefix) as import('$app/types').Pathname ), - { replaceState: true } + { replaceState: true, keepFocus: true } ); } }); diff --git a/src/routes/(app)/storage/+page.svelte b/src/routes/(app)/storage/+page.svelte index ad87eb38..d27b19e8 100644 --- a/src/routes/(app)/storage/+page.svelte +++ b/src/routes/(app)/storage/+page.svelte @@ -150,7 +150,7 @@ -

{m.storage_buckets_subtitle()}

+

{m.storage_buckets_subtitle()}

diff --git a/src/routes/(app)/trino/+page.svelte b/src/routes/(app)/trino/+page.svelte index c2debd90..a322350f 100644 --- a/src/routes/(app)/trino/+page.svelte +++ b/src/routes/(app)/trino/+page.svelte @@ -7,7 +7,6 @@ import IconMenu from 'virtual:icons/material-symbols/menu'; import IconPanelClose from 'virtual:icons/material-symbols/left-panel-close-outline'; import IconPanelOpen from 'virtual:icons/material-symbols/left-panel-open-outline'; - import IconExpandMore from 'virtual:icons/material-symbols/expand-more'; import IconOpenInNew from 'virtual:icons/material-symbols/open-in-new'; import MonacoEditor from '$lib/components/editor/MonacoEditor.svelte'; import CatalogBrowser from '$lib/components/catalog/CatalogBrowser.svelte'; @@ -17,7 +16,7 @@ import { superForm } from 'sveltekit-superforms'; import { zod4Client } from 'sveltekit-superforms/adapters'; import { ConnectionSchema, type ConnectionMessage } from './validation.js'; - import TabBar from '$lib/components/TabBar.svelte'; + import TabBar, { tabElementId } from '$lib/components/TabBar.svelte'; import { tabStore, MAX_SQL_LENGTH } from '$lib/stores/tab-store.svelte.js'; import { getOrCreateQueryRunner, @@ -33,6 +32,7 @@ } from '$lib/editor/split-statements.js'; import type { PageData } from './$types'; import StatementResult from '$lib/components/trino/StatementResult.svelte'; + import RunButton, { type RunMode } from '$lib/components/trino/RunButton.svelte'; import { getLocale } from '$lib/paraglide/runtime.js'; let { data }: { data: PageData } = $props(); @@ -79,6 +79,8 @@ } let catalogBrowserOpen = $state(getInitialCatalogOpen()); let monacoEditor = $state(undefined); + // Running needs the editor's cursor and selection, which exist only once Monaco has loaded + let editorReady = $state(false); let mobileCatalogOpen = $state(false); const viewStates = new SvelteMap< @@ -320,7 +322,6 @@ const hasSelection = $derived(monacoEditor?.getSelection() != null); - type RunMode = 'cursor' | 'all'; let runMode = $state('cursor'); function handleRun() { @@ -336,20 +337,6 @@ } } - function selectRunMode(mode: RunMode) { - runMode = mode; - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); - } - } - - const runButtonLabel = $derived.by(() => { - if (runMode === 'all') { - return hasSelection ? m.trino_run_selected() : m.trino_run_all(); - } - return m.trino_run_at_cursor(); - }); - // Clear submitted statements when the user edits, so highlighting stops. const highlightOffsets = $derived.by(() => { const progress = runner.scriptProgress; @@ -357,8 +344,6 @@ return submittedStatements[progress.currentStatementIndex] ?? null; }); - const runShortcutLabel = $derived(runMode === 'cursor' ? 'Ctrl+↵' : 'Ctrl+Shift+↵'); - function handleKeydown(event: KeyboardEvent) { // Monaco handles its own keybindings — skip if the event originated from the editor. if (event.target instanceof HTMLElement && event.target.closest('[data-ready]')) return; @@ -366,7 +351,8 @@ if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key === 'Enter') { event.preventDefault(); handleRunAll(); - } else if ((event.ctrlKey || event.metaKey) && event.key === 'Enter') { + } else if ((event.ctrlKey || event.metaKey) && event.key === 'Enter' && editorReady) { + // Running at the cursor needs the editor, like the Run button event.preventDefault(); handleRunAtCursor(); } @@ -427,7 +413,7 @@ class="border-base-300 flex items-center justify-between border-b px-3 py-2" style="width: {catalogResize.width}px" > - {m.trino_catalog_browser()} + {m.trino_catalog_browser()}
- {m.trino_catalog_browser()} + {m.trino_catalog_browser()}
@@ -614,6 +600,10 @@ m.trino_tab_close({ name: item.label })} + addLabel={m.trino_tab_new()} + renameLabel={m.trino_tab_rename()} onSelect={handleTabSelect} onClose={handleTabClose} onAdd={handleTabAdd} @@ -639,7 +629,7 @@ rounded-b-xl border " role="tabpanel" - aria-labelledby="tab-{tabStore.activeTabId}" + aria-labelledby={tabElementId(tabStore.activeTabId)} >
{m.trino_editor_label()} + {m.trino_editor_label()} {#if charLimitReached} {m.trino_editor_char_limit_reached({ @@ -694,115 +684,20 @@ {m.trino_cancel_query()} {/if} - {#snippet runOption(label: string, shortcut: string)} - - {label} - {shortcut} - - {/snippet} - - -
-
- - -
-
+
{#if runner.scriptProgress} - + {m.trino_script_progress({ current: runner.scriptProgress.currentStatementIndex + 1, total: runner.scriptProgress.totalStatements @@ -847,7 +742,7 @@ {runner.error} {/if} {#if runner.state === 'RUNNING'} - {Math.round(runner.progress.progressPercentage)}% {/if} {#if progressInfo} - {progressInfo} + {progressInfo} {/if} {#if runner.currentTrinoQueryUrl}
- {m.trino_results_label()} + {m.trino_results_label()}
@@ -885,7 +780,7 @@ {/each} {:else if !isActive} -

{m.trino_results_empty()}

+

{m.trino_results_empty()}

{/if}
diff --git a/src/routes/auth/login/+page.svelte b/src/routes/auth/login/+page.svelte index 99287695..a97b8091 100644 --- a/src/routes/auth/login/+page.svelte +++ b/src/routes/auth/login/+page.svelte @@ -41,7 +41,7 @@

{m.auth_login_title()}

-

{m.auth_login_subtitle()}

+

{m.auth_login_subtitle()}

{#if error}