Open the capture sheet from the dashboard's add button on phones

On the phone shell the header's 'Add' opened a dropdown while the plus
in the bottom nav, one thumb below, opened the capture sheet. The header
button now opens the same sheet there; the desktop rail keeps the
dropdown. The onboarding tour anchors on whichever of the two is laid
out.
This commit is contained in:
Marc-André Bombeck committed 2026-10-08 18:36:46 +02:00
1 parent b53782810f
commit 6ca33882f1
3 files changed
+51 -14

No files matched your search

+9 -4
View File
@@ -141,7 +141,10 @@ test.describe("v1.4.33 F2 — onboarding tour passes clicks through to the page"
// so a naive "dim everything outside the spotlight rect" overlay
// covers it. The fix makes the entire tour layer pointer-events:
// none — every dim panel and the spotlight ring are visual only.
const quickAddButton = page.locator('[data-tour-id="dashboard-quick-add"]');
// One add button per shell carries the tour id; the other is hidden.
const quickAddButton = page
.locator('[data-tour-id="dashboard-quick-add"]')
.filter({ visible: true });
await expect(quickAddButton).toBeVisible();
// Forensic guard — at the quick-add button's centre, every element
@@ -153,9 +156,11 @@ test.describe("v1.4.33 F2 — onboarding tour passes clicks through to the page"
// the stack is blocking, instead of just observing the symptom
// ("button click did not toggle aria-expanded").
const tourBlockers = await page.evaluate(() => {
const btn = document.querySelector<HTMLElement>(
'[data-tour-id="dashboard-quick-add"]',
);
const btn = [
...document.querySelectorAll<HTMLElement>(
'[data-tour-id="dashboard-quick-add"]',
),
].find((el) => el.getClientRects().length > 0);
if (!btn) return ["no-button"];
const rect = btn.getBoundingClientRect();
const x = rect.left + rect.width / 2;
+24 -2
View File
@@ -10,7 +10,7 @@
* Extracted from the dashboard page; the page owns the open-state the
* dropdown items set via `onQuickEntry`.
*/
import { useMemo } from "react";
import { useMemo, useState } from "react";
import Link from "next/link";
import {
Activity,
@@ -37,6 +37,7 @@ import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
import { hourInTz } from "@/lib/tz/format";
import {
CAPTURE_KIND_ORDER,
CapturePicker,
visibleCaptureKinds,
} from "@/components/layout/capture-picker";
import type { QuickEntryDialog } from "@/components/dashboard/quick-entry-sheets";
@@ -49,6 +50,7 @@ export function DashboardHeader({
const { t } = useTranslations();
const { user } = useAuth();
const mounted = useMounted();
const [captureOpen, setCaptureOpen] = useState(false);
// The quick-add offers four kinds, and each one is asked about its own
// section: a reading under `measurements`, a dose under `medications`, a
// mood entry under `mind`, a workout in one's own record only. The coarse `canAdd` this used to gate the whole
@@ -149,6 +151,26 @@ export function DashboardHeader({
</Link>
</Button>
)}
{/* On the phone shell the add button opens the same capture
picker sheet the bottom nav's plus opens: one way to add a
thing, not a dropdown here and a sheet one thumb below. */}
{canAddAnything && (
<Button
size="default"
className="shell-desktop:hidden min-h-11"
onClick={() => setCaptureOpen(true)}
aria-haspopup="dialog"
aria-expanded={captureOpen}
data-slot="dashboard-quick-add-capture"
data-tour-id="dashboard-quick-add"
>
<Plus className="h-4 w-4" />
{t("common.add")}
</Button>
)}
{canAddAnything && (
<CapturePicker open={captureOpen} onOpenChange={setCaptureOpen} />
)}
{canAddAnything && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -166,7 +188,7 @@ export function DashboardHeader({
+ label keep the same visual contract. */}
<Button
size="default"
className="min-h-11 sm:min-h-9"
className="shell-mobile:hidden min-h-11 sm:min-h-9"
data-tour-id="dashboard-quick-add"
>
<Plus className="h-4 w-4" />
+18 -8
View File
@@ -102,12 +102,26 @@ interface SpotlightRect {
* not in the DOM or has zero dimensions — caller renders a centred
* tooltip in that case.
*/
/**
* The rendered element carrying a tour id. A control can exist twice with the
* same id, one per shell (the dashboard's add button is a dropdown on the
* desktop rail and a sheet trigger on the phone shell), with the other one
* hidden; the stop anchors on the one that is actually laid out.
*/
function findTourTarget(targetId: string): HTMLElement | null {
const all = document.querySelectorAll<HTMLElement>(
`[data-tour-id="${CSS.escape(targetId)}"]`,
);
for (const el of all) {
if (el.getClientRects().length > 0) return el;
}
return all[0] ?? null;
}
function measureTarget(targetId: string | null): SpotlightRect | null {
if (!targetId) return null;
if (typeof document === "undefined") return null;
const el = document.querySelector<HTMLElement>(
`[data-tour-id="${CSS.escape(targetId)}"]`,
);
const el = findTourTarget(targetId);
if (!el) return null;
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return null;
@@ -356,11 +370,7 @@ export function OnboardingTour({
const MAX_ATTEMPTS = 40;
const settleAndMeasure = () => {
if (cancelled) return;
const el = stop.targetId
? document.querySelector<HTMLElement>(
`[data-tour-id="${CSS.escape(stop.targetId)}"]`,
)
: null;
const el = stop.targetId ? findTourTarget(stop.targetId) : null;
if (el) {
// Bring the anchor into view BEFORE measuring — anchors below the
// fold positioned the popover under the viewport bottom otherwise.