role=toolbar with a roving tabindex: Arrow keys move between buttons,
one Tab stop, Tab leaves the toolbar, and the tab stop stays valid when
buttons become disabled. Covered by an e2e keyboard-navigation spec.
Also fixes the table-edit :disabled bindings, which referenced
editor.can().X as a property (always truthy) so those buttons never
actually disabled — surfaced by review while testing the roving focus.
Claude-Session: https://claude.ai/code/session_018prq5WqRL38EGhPNQKqZUP
The white hero heading sat directly on the photo, where its contrast
depends on the image content. A bottom-weighted gradient scrim keeps it
readable across the whole picture.
Claude-Session: https://claude.ai/code/session_018prq5WqRL38EGhPNQKqZUP
Opens the existing alt-text prompt right after an image is inserted
(upload, paste and URL flows) so every new image gets a description.
Cancelling leaves the image without alt text, and the node is deselected
afterwards so the next insert appends instead of replacing.
Claude-Session: https://claude.ai/code/session_018prq5WqRL38EGhPNQKqZUP
Each attachment row was a button containing the download, copy-URL,
delete and set-cover buttons — interactive elements nested inside an
interactive element, which is invalid and confuses assistive-technology
focus (axe: nested-interactive). The row is now a non-interactive
container: the filename (and thumbnail) is the trigger that opens or
downloads the attachment, and the action buttons are its siblings.
Claude-Session: https://claude.ai/code/session_018prq5WqRL38EGhPNQKqZUP
The fuzzy mirror test is ParadeDB-only and fails without the previous
commit (child returned as a second root). The multi-parent test pins the
NOT EXISTS semantics from #3197: one in-scope and one out-of-scope
parent must still nest the child.
Part of #2954
The parent root-membership mirror always used the ILIKE fallback, so on
ParadeDB a parent matching the search only via fuzzy/token matching kept
its child as a duplicate root, and a substring-only ILIKE match could
hide a child under a parent absent from the results. The assumed
limitation no longer holds: pg_search plans ||| against the aliased
parent_tasks inside the correlated NOT EXISTS as a regular BM25 scan
(verified on 0.24.1), and score-free predicates never triggered the
"unsupported query shape" error on older versions either.
Part of #2954
Add disclosure semantics to dropdown/popup triggers (aria-expanded, Escape to
close with focus return, drop the incorrect role=menu), aria-sort plus
per-column labels on the table view, a debounced polite live region announcing
quick-actions result counts, aria-pressed on editor toggle buttons, and an
aria-label on the rich-text editing region.
Claude-Session: https://claude.ai/code/session_018prq5WqRL38EGhPNQKqZUP
echo v5.3.0 reverted to v4 behavior where every group with middleware
implicitly registers a 404 route at its prefix. Our route setup creates
multiple groups with the same prefix (rate-limit subgroups of /api/v1),
which now panics with a duplicate-route error at startup.
Five PUT-routed "create" endpoints document @Success 200, but they all
dispatch through the shared CreateWeb handler (pkg/web/handler/create.go),
which always responds 201 Created. Every other CreateWeb-routed endpoint
(labels, teams, filters, task relations/comments/assignees/duplicate,
task-labels) already correctly documents 201 - these five are stragglers
with the same stale annotation.
Affected endpoints:
- PUT /tokens
- PUT /projects/{id}/views/{view}/buckets
- PUT /projects/{id}/webhooks
- PUT /{kind}/{id}/reactions
- PUT /projects/{project}/views
Doc-only change, no behavior change. Regenerated pkg/swagger/* via
`swag init -g ./pkg/routes/routes.go --parseDependency -d . -o ./pkg/swagger`
(the command mage generate:swagger-docs runs) so the generated docs match
the annotations.
Adds translated aria-labels to the 42 icon-only BaseButton/XButton
usages the new lint rule found — attachment actions, assignee/label
remove buttons, multiselect clear buttons, table sort triggers, the
TipTap bubble menu (which only had v-tooltip, which sets no accessible
name), datepicker toggles, and more. Labels reuse the existing tooltip
translation keys where present and interpolate item names where
available (e.g. "Remove {user} as assignee").
With zero remaining violations the rule is raised from warn to error.
Claude-Session: https://claude.ai/code/session_018prq5WqRL38EGhPNQKqZUP
BaseButton renders a bare button and does not enforce a name, so icon-only
usages without aria-label are announced as just "button" — the a11y audit
found this pattern regenerating across the codebase (42 current instances,
including the TipTap bubble menu where v-tooltip provides no accessible
name). The new vikunja/icon-button-accessible-name rule flags icon-only
BaseButton/XButton usages that have no aria-label, aria-labelledby, title,
visible text, or sr-only text; unknown child components are assumed to
render text to avoid false positives.
Set to 'warn' until the existing instances get aria-labels, then raise to
'error'.
Claude-Session: https://claude.ai/code/session_018prq5WqRL38EGhPNQKqZUP
Parametrised error translations (invalid api token permission, invalid
timezone, invalid claim data) rendered their placeholders as empty text
because the backend only baked the values into the English message and
the frontend translated the error code without any interpolation params.
Carry the values as i18n_params on error responses, keyed by the
placeholder names of the frontend's error translations, on both v1 and
the v2 problem+json body, and pass them through when translating the
toast. A 14002 toast now reads e.g. "The permission time_entries of
group tasks is invalid." instead of "The permission of group is
invalid."
The .input:focus reset left text fields and the contenteditable task
title without any focus indication. Because :focus-visible matches mouse
clicks on text fields, keyboard modality is tracked via a user-is-tabbing
class on the root element instead.
The task link had tabindex="-1" and the document-level list navigation
handler prevented default on every Enter press, so keyboard users could
not open a task from the list view at all.
On touch devices flatpickr swaps in a native datetime-local input and
writes the pre-filled date including wall-clock seconds as its default
value. With no step attribute and a date-only (thus ignored) min, those
seconds become the step base, so every value picked through the native
UI (always :00 seconds) fails validation as a stepMismatch and the
browser blocks the form submit.
Zeroing seconds on the initial date puts the step base on the :00 grid
where native picker values land.
Fixesgo-vikunja/vikunja#3175