Marking a task done via the list-view checkbox deferred the entire update
— including the network request — by 300ms to play the check animation. If
the page was torn down within that window (a refresh, tab close, or leaving
the app), the request was never sent and the save was silently lost. For
repeating tasks this is especially confusing: the due date never advances,
yet the checkbox un-checks itself anyway, so the failure looks identical to
success.
Fire the request immediately with the intended done value snapshotted, and
defer only the animation-coupled follow-up (result swap, pop sound, toast).
The optimistic v-model state already drives the check animation during the
300ms, so nothing visual is lost.
The Kanban task popup renders the description editor inside a native
<dialog> opened via showModal(), which lives in the browser's top-layer.
inputPrompt appended its URL <input> to document.body, so it was painted
behind the top-layer dialog (z-index cannot beat the top-layer) and could
not be focused through the dialog's focus trap. As a result clicking "Link"
in the popup did nothing, while it worked on the full task page (no modal).
Thread the TipTap editor through inputPrompt and append the prompt to
getPopupContainer(editor) — the open dialog ancestor when present, falling
back to document.body otherwise, so non-modal usage is unchanged. This is
the same helper the slash menu and mentions already use to escape the
top-layer (#1746).
Fixes#2940
The Kanban task detail opens as a native <dialog> via showModal(), which
paints in the browser top-layer. Floating UI appended to document.body
(or teleported to <body>) then renders behind the dialog regardless of
z-index, matching the bug class of #2940 / #1746 / #1899 / #1929.
- Emoji autocomplete popup: append to getPopupContainer(editor) (the open
dialog ancestor, else body), the same helper the slash menu and mentions
already use. Also switch its unmount to popupElement.remove() so it works
no matter which container it was appended to.
- Attachment dropzone overlay: teleport into the topmost open
dialog.modal-dialog instead of always <body>, mirroring Notification.vue,
so the drag-and-drop hint is visible while a task detail dialog is open.
Centered default/hint-modal content used translate(-50%, -50%) with no
height cap, so a taller-than-viewport modal (e.g. project background
settings with the Unsplash grid) pushed its top edge above the viewport
where the container's overflow can't reach it — the upload button became
unreachable on short screens.
Cap the centered content to the viewport and scroll inside it, mirroring
the height limit the .top (quick actions) variant already has. The mobile
breakpoint resets both so the fullscreen layout keeps flowing in
.modal-container.
Focusing the task bar SVG `<g role="slider">` inside the
`overflow-x:auto` `.gantt-container` triggered Firefox's focus-induced
scroll-into-view, which jumped the scroll container back toward
`scrollLeft=0` (today). Pass `{ preventScroll: true }` to `focus()` so
selecting a bar keeps the current scroll position. Chromium scrolls
minimally on focus so it never manifested there.
Fixes#2728
The quick actions menu (cmd+k) rendered without any background and grew
beyond the viewport:
- Its card visuals came from the global Bulma .card styles, which were
dropped when Card.vue got its own scoped copy — QuickActions is the
only place using a bare class="card" div, so it lost background,
border and shadow. Give it its own card styles.
- Its height limit came from Bulma's .modal-content max-height, lost
when the Bulma modal import was dropped in the native-dialog refactor.
The :deep(.modal-content) position override in QuickActions never
matched (.modal-content is an ancestor of the scoped selector, not a
descendant). Replace both with a proper `top` modal variant that
anchors the content 3rem below the top edge and caps its height,
resolving the FIXME asking for exactly that option.
- The dark scrim never showed: Chromium intermittently stops painting a
styled ::backdrop (after subtree re-renders, or while display is
transitioned) even though getComputedStyle reports the color. Move
the scrim onto the viewport-filling dialog element itself — same as
the old div-based .modal-mask — and drop the display/allow-discrete
transitions, which the JS-timed close fade never needed.
The `service.allowiconchanges` config option was ignored. On the web ui the
value injected into index.html by the api was immediately overwritten by a
hardcoded `window.ALLOW_ICON_CHANGES = true` in a later inline script, so the
configured value never took effect. The desktop app never received the
injected value at all, since it serves the bundled frontend from its own local
server and only talks to the api for data.
Expose the option via the /info endpoint and read it from the config store,
which is the only channel that reaches both the web ui and the desktop app.
The brittle window injection and its hardcoded default are removed in favor of
this single source of truth.
https://claude.ai/code/session_01HAXTJNsDcfsB4hwDNKTECb
Each rendered comment gets a "Reply" action (shown whenever the
viewer has write access, regardless of authorship). Clicking it
prefills the comment editor with a <blockquote data-comment-id="X">
wrapping the parent body so the canonical reply marker is the
blockquote itself.
A Vue NodeView on the blockquote extension renders an author
header + chevron when an injected commentReplyContext can resolve
the parent. The chevron scrolls to and briefly highlights the
original. Quotes whose parent isn't in the in-memory list (deleted,
on another page) render a degraded header with the chevron hidden.
Extend the default Blockquote with a `commentId` attribute that
round-trips through HTML as `data-comment-id`. This single attribute
is the canonical record of a reply: it survives TipTap serialize /
parse so the backend listener and the in-app renderer can both find
the parent comment without a separate schema field.
A <dialog> opened via showModal() lives in the browser's top layer, which
renders only on the first page during print — top-layer elements are
viewport-anchored and don't paginate. CSS overrides like position: static
have no effect since top-layer membership is browser-managed.
Swap to a non-modal dialog on beforeprint (removes it from the top layer
so content flows in normal document order) and back to modal on
afterprint. The accompanying @media print rules reset the dialog's fixed
positioning and overflow so the non-modal dialog can paginate freely.
The .avatar img in User.vue relied solely on the width/height HTML
attributes for sizing. Those are presentational hints with zero CSS
specificity, so Bulma's global reset (img { height: auto; max-width: 100% })
overrode them. While avatarSrc was still resolving (initial src=""),
the browser had no intrinsic dimensions to compute the auto height from
and fell back to the broken-image box (~96px in Chrome), then snapped
to the real size once the blob URL loaded.
Set inline-size/block-size explicitly via a CSS custom property bound
to the avatarSize prop so the rendered size is locked regardless of
load state or the Bulma reset.
The sticky bucket footer had no z-index, so the absolutely positioned
`.handle` overlays on each task (z-index: 1, used to capture taps on
touch devices) stacked above the Add Task button. Tapping the button
where a task scrolled behind it would open that task instead of opening
the new-task input.
Route the create flow through taskStore.createNewTask so titles typed
into the related-task input get parsed for labels, priority, assignees,
due dates and cross-project targets - matching the main add-task input.
Also surface the quick-add-magic hint next to the field.
The pseudo-element that extends the checkbox hit target also covered
label text content, which broke Playwright actionability checks for
clicks on text inside wider FancyCheckbox labels (e.g. the "Show
Archived" toggle on the projects list page).
Move the rule out of BaseCheckbox and into SingleTaskInProject's deep
override, where the label slot is already hidden via display: none, so
no neighboring content can be intercepted.
A pseudo-element on the label provides a 44x44 minimum hit area
centered on the visible icon. Visible size and surrounding layout
are unchanged. Addresses misclicks on the task list view checkbox
where ~50% of taps would open the task detail instead of toggling
done.
The flatpickr time inputs hardcoded `time_24hr: true`, so users who
selected the 12-hour format in their settings still got a 24-hour
picker — even though the displayed dates respected the preference.
Bind `time_24hr` to the existing `useTimeFormat` composable in:
- DatepickerInline (start/end/due dates and absolute reminders)
- DeferTask (defer due date)
- ApiTokenForm (API token expiry)
Reported at https://community.vikunja.io/t/4492.
The Modal mounts the <dialog> via v-if and calls showModal() in a
follow-up flush, so v-focus runs while the dialog is still closed and
its focus() call is dropped. The existing rAF retry was gated on
quick-add mode, leaving Ctrl+K in the regular app with no focused
input. Run the retry whenever the quick actions become active and keep
the command pre-selection scoped to quick-add mode.
Mouse event emulation from taps on touch devices caused the glance
tooltip to appear unexpectedly with no reliable way to dismiss it.
Gate the tooltip behind a `(hover: hover) and (pointer: fine)` media
query so it only activates on devices with a real pointer.
Wire aria-invalid, aria-describedby and role=alert on the form
primitive components so errors raised directly on FormInput or
FormSelect are announced by assistive tech and programmatically
linked to the control.