diff --git a/e2e/tests/search.test.ts b/e2e/tests/search.test.ts index d540117f..e9332539 100644 --- a/e2e/tests/search.test.ts +++ b/e2e/tests/search.test.ts @@ -83,8 +83,9 @@ test.describe("search", () => { await searchFor(dialog, "marmalade", result) // the text exists on the draft alone, so that is the branch the - // hit names and the one the link opens - await expect(result.getByTestId("search-result-branch")).toHaveText("draft") + // hit names and the one the link opens. The branch name is the last + // span of the result's meta line + await expect(result.locator("span:last-child")).toHaveText("draft") await result.click() await expect(page).toHaveURL(/New-Page-[a-z0-9]{20}\?branch=[a-z0-9]{20}/) diff --git a/web/CLAUDE.md b/web/CLAUDE.md index 601078e8..79148df2 100644 --- a/web/CLAUDE.md +++ b/web/CLAUDE.md @@ -117,13 +117,13 @@ Form validation (vee-validate) uses its own internal messages — see [README.md - **shadcn-vue** components live in [app/components/shadcn/ui/](app/components/shadcn/ui/) with prefix `ShadcnUi`. Configured in [components.json](components.json). - **Tailwind v4** via `@tailwindcss/vite`, theme in [app/assets/css/main.css](app/assets/css/main.css). - Icons via `@nuxt/icon` (CSS mode). The client bundle holds the icons the scanner finds in app code, plus — on the desktop build only — the document title icons listed by `selectableIconList()` in [app/utils/icon.ts](app/utils/icon.ts). The web build resolves those on demand through the server so they stay off the startup path. The picker itself does not go through ``: [modules/icon-picker-css.ts](modules/icon-picker-css.ts) generates one stylesheet with a rule per selectable icon, served as `virtual:icon-picker.css` and imported by the picker provider once the page's load event has fired. Custom SVGs live in [app/assets/custom-icons/](app/assets/custom-icons/) and are reachable under the `custom-icons:` prefix. -- **Open a11y bug: dialogs have no `DialogDescription`.** Every modal renders its description as a plain `

`, so the `aria-describedby` reka-ui puts on `DialogContent` points at nothing — screen readers get no description, and reka-ui warns once per mount. **This is scheduled to be fixed, not accepted.** The warning is suppressed in [vitest.config.ts](vitest.config.ts)'s `onConsoleLog` purely to keep test output readable in the meantime; that branch comes out with the fix, and the warning is still visible in `nuxt dev`. Note that routing the text through `ShadcnUiDialogDescription` is *not* a drop-in fix: the wrapper runs its class through `cn()`, and default tailwind-merge reads the custom `text-2sm` as a colour utility and drops it in favour of the wrapper's `text-sm`. Closing this needs either reka-ui's `DialogDescription` imported directly (no `cn()`) or `extendTailwindMerge` teaching `cn()` the custom text scale. +- **Every dialog describes itself.** reka-ui points `DialogContent`'s `aria-describedby` at a `DialogDescription`, so each dialog and sheet renders one — `ShadcnUiDialogDescription` (or the sheet variant) with `sr-only` when nothing visible plays that role, `as-child` around an `` when the visible description is interpolated. The settings action components render theirs inside the `ActionModal` around them, which is why their tests mount through `mountUnderDialogRoot`. A dialog missing one makes reka-ui warn once per mount in `nuxt dev` and in test output. ## Formatting & TS Prettier uses **tabs**, no semicolons, trailing commas — see [prettier.config.js](prettier.config.js). -ESLint ([eslint.config.mjs](eslint.config.mjs)) runs **type-aware**: `eslint.config.typescript.tsconfigPath` in `nuxt.config.ts` switches the generated Nuxt preset to the `*-type-checked` rule sets, on top of which the config adds `eslint:recommended` (the Nuxt preset ships no base JS rules), typescript-eslint **strict + stylistic**, and `@intlify/eslint-plugin-vue-i18n`. Consequences worth knowing: +ESLint ([eslint.config.mjs](eslint.config.mjs)) runs **type-aware**: `eslint.config.typescript.tsconfigPath` in `nuxt.config.ts` switches the generated Nuxt preset to the `*-type-checked` rule sets, on top of which the config adds `eslint:recommended` (the Nuxt preset ships no base JS rules), typescript-eslint **strict + stylistic**, `@intlify/eslint-plugin-vue-i18n`, and `eslint-plugin-vuejs-accessibility` (its recommended set; the shadcn wrappers are exempt from the label-association and static-element rules, since the association is made where they are used). Consequences worth knowing: - Linting builds a full TS program, so a cold `check-eslint` takes about a minute. Runs are cached (`node_modules/.cache/eslint`) — unchanged files diff --git a/web/app/components/DocumentDeletionModal.vue b/web/app/components/DocumentDeletionModal.vue index c25d5481..4d952715 100644 --- a/web/app/components/DocumentDeletionModal.vue +++ b/web/app/components/DocumentDeletionModal.vue @@ -57,18 +57,19 @@ async function deleteDocument() {

- - - + + + + +
diff --git a/web/app/components/SearchModal.nuxt.test.ts b/web/app/components/SearchModal.nuxt.test.ts index e198f1ee..b314e344 100644 --- a/web/app/components/SearchModal.nuxt.test.ts +++ b/web/app/components/SearchModal.nuxt.test.ts @@ -224,8 +224,11 @@ describe("", { concurrent: false }, () => { await search("r") + // a result's spans are its icon, its type and its branch name, in that + // order, and highlights are s — so the branch is the only span + // that closes its parent const branches = [ - ...document.body.querySelectorAll("[data-testid='search-result-branch']"), + ...document.body.querySelectorAll("a span:last-child"), ].map((el) => el.textContent.trim()) expect(branches).toEqual(["main", "draft"]) diff --git a/web/app/components/SearchModal.vue b/web/app/components/SearchModal.vue index 62b020fd..cff17348 100644 --- a/web/app/components/SearchModal.vue +++ b/web/app/components/SearchModal.vue @@ -209,10 +209,7 @@ function resultTypeText(type: string) { {{ resultTypeText(result.type) }} - + {{ result.branchName }}
diff --git a/web/app/components/TagDeletionModal.vue b/web/app/components/TagDeletionModal.vue index a73b91dd..171a6167 100644 --- a/web/app/components/TagDeletionModal.vue +++ b/web/app/components/TagDeletionModal.vue @@ -57,18 +57,19 @@ async function deleteTag() {
- - - + + + + +
("[data-name]") if (!cell) { return @@ -230,6 +230,8 @@ function handleEscape(e: KeyboardEvent) { @scroll="handleScroll" @mouseover="handleGridHover" @mouseleave="hideTooltip" + @focusin="handleGridHover" + @focusout="hideTooltip" > diff --git a/web/app/components/editor/NameEditor.nuxt.test.ts b/web/app/components/editor/NameEditor.nuxt.test.ts index 129aaee1..6642accb 100644 --- a/web/app/components/editor/NameEditor.nuxt.test.ts +++ b/web/app/components/editor/NameEditor.nuxt.test.ts @@ -125,10 +125,10 @@ async function openActionMenu(wrapper: VueWrapper) { await nextTick() } -// the icon picker comes first, then the review action and the menu that -// switches between the review actions +// the hook handle and the icon picker come first, then the review action +// and the menu that switches between the review actions function actionMenuTrigger(wrapper: VueWrapper) { - return wrapper.findAll("button")[2] + return wrapper.findAll("button")[3] } function hookHandle(wrapper: VueWrapper) { diff --git a/web/app/components/editor/NameEditor.vue b/web/app/components/editor/NameEditor.vue index da4b8b9d..32822b7d 100644 --- a/web/app/components/editor/NameEditor.vue +++ b/web/app/components/editor/NameEditor.vue @@ -393,20 +393,24 @@ async function executeReviewableAction() { @update:open="(v: boolean) => (hookMenuOpen = v)" > -
-
+ -
+
+ {{ $t("editor.hook-handle.screen-reader-hint") }} -
+ {