Problem: opening Send Note on iPad, tapping the search field opened the
soft keyboard which covered the recipients list and the Send button —
the dialog stayed centered on the full layout viewport.
Fix (artifacts/tx-os/src/components/notes/send-note-dialog.tsx):
Two-tier keyboard detection:
1. Primary — `vv.keyboardInset > 0` from useVisualViewport. Works on
desktop touch laptops, iPad Safari (regular tab), Android Chrome.
When true, override DialogContent style with
top = vv.offsetTop + vv.height/2 − 48 (QuickType inset)
transform: translate(-50%, -50%)
so the dialog recenters around the visible viewport region.
2. Fallback — `searchFocused && matchMedia('(pointer: coarse)')`.
Only used when visualViewport does NOT report an inset, which is
the iPad PWA (Add to Home Screen) case explicitly called out in
components/ui/dialog.tsx comment. Top-anchors at 6vh with
maxHeight 50dvh so the dialog never crosses the screen midpoint.
3. Neither — pass NO style at all so Radix's default centered layout
is fully restored (no leftover transition or top).
Touch-only fallback gating (matchMedia pointer:coarse) prevents the
earlier desktop-regression flagged by code review — focusing the
search on a desktop never repositions the dialog.
Other changes:
- onFocus/onBlur on the search input feed the fallback signal.
- Blur is debounced 150ms so tapping a recipient row mid-blur doesn't
recenter the dialog out from under the finger.
- useEffect cleanup clears pending blur timeout on unmount.
Verification: tsc clean. Architect approved the touch-gated fallback;
follow-up review reconciled the diff with the spec's primary
visualViewport-driven path.
Problem: opening Send Note on iPad PWA, tapping the search field popped
up the soft keyboard which covered the entire people list and the Send
button. The previous fix relied on visualViewport to shrink the list,
but iPad PWAs don't reliably resize visualViewport when the keyboard
opens (see the note in components/ui/dialog.tsx referencing the
reverted #622–#624 global attempts).
Fix (artifacts/tx-os/src/components/notes/send-note-dialog.tsx):
- Track search-input focus directly — the most reliable "keyboard is
about to open" signal on touch devices, independent of visualViewport.
- Gate the override behind `matchMedia('(pointer: coarse)')` so it
only fires on touch-primary devices (iPad/iPhone/Android) and never
on desktop or trackpad laptops — addressing the architect's
cross-platform regression flag.
- When the gate is active, override DialogContent's Radix center
anchor (top:50%; translate(-50%,-50%)) with `top:6vh;
translate(-50%,0); maxHeight:50dvh`. The dialog grows downward from
a fixed top edge so it always fits in the upper half of the screen,
leaving the lower half free for the keyboard.
- Cap `listMaxHeight` to ~50% of innerHeight in the same gated
condition, as a backstop where visualViewport stays full-height.
- Debounce blur by 150ms so a tap on a recipient row doesn't lose
the row mid-tap to a center-jump animation; clear the timeout on
unmount.
- Added 120ms ease transition on top/transform so the layout swap
doesn't snap.
Deviations: original plan used vv.offsetTop+vv.height/2 to reposition,
but that depends on the same unreliable visualViewport. Switched to
a focus + coarse-pointer driven approach which works on iPad PWA.
Verification: tsc clean. Architect re-review pending after the touch
gate was added (first review flagged the missing platform guard,
which this revision addresses directly).
Problem: opening Send Note on iPad PWA, tapping the search field popped
up the soft keyboard which covered the entire people list and the Send
button. The previous fix relied on visualViewport to shrink the list,
but iPad PWAs don't reliably resize visualViewport when the keyboard
opens (see the note in components/ui/dialog.tsx referencing the
reverted #622–#624 global attempts).
Fix (artifacts/tx-os/src/components/notes/send-note-dialog.tsx):
- Track search-input focus directly — the most reliable "keyboard is
about to open" signal on touch devices, independent of visualViewport.
- Gate the override behind `matchMedia('(pointer: coarse)')` so it
only fires on touch-primary devices (iPad/iPhone/Android) and never
on desktop or trackpad laptops — addressing the architect's
cross-platform regression flag.
- When the gate is active, override DialogContent's Radix center
anchor (top:50%; translate(-50%,-50%)) with `top:6vh;
translate(-50%,0); maxHeight:50dvh`. The dialog grows downward from
a fixed top edge so it always fits in the upper half of the screen,
leaving the lower half free for the keyboard.
- Cap `listMaxHeight` to ~50% of innerHeight in the same gated
condition, as a backstop where visualViewport stays full-height.
- Debounce blur by 150ms so a tap on a recipient row doesn't lose
the row mid-tap to a center-jump animation; clear the timeout on
unmount.
- Added 120ms ease transition on top/transform so the layout swap
doesn't snap.
Deviations: original plan used vv.offsetTop+vv.height/2 to reposition,
but that depends on the same unreliable visualViewport. Switched to
a focus + coarse-pointer driven approach which works on iPad PWA.
Verification: tsc clean. Architect re-review pending after the touch
gate was added (first review flagged the missing platform guard,
which this revision addresses directly).
Refactor notification sound playback to use per-bucket throttling, adjust socket warmup, and ensure push notifications are sent even if the client is considered connected.
Refactor notification sound playback to use per-bucket throttling, adjust socket warmup, and ensure push notifications are sent even if the client is considered connected.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 3dcee04b-6717-4c1f-a172-b1f9c2febbe0
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/qrzw3bH
Replit-Helium-Checkpoint-Created: true
- /auth/logout: delete all push_subscriptions rows for the user before
destroying the session. Best-effort with logger.warn on failure so an
operator can correlate any leaked-ring report to a real DB error.
- home.tsx handleLogout: call pushSub.disable() before the logout
mutation, raced against a 1.5s timeout so a stalled service worker
cannot block sign-out.
- executive-meeting-scheduler: switch the eligibility filter from
denylist (ne cancelled + ne completed) to whitelist
(eq status='scheduled'). Postponed / rescheduled / future statuses
can no longer trigger false 5-minute reminders.
- docker-compose.yml: pin TZ=Asia/Riyadh on postgres, api, and web
services so the scheduler's naive date/time math matches the
operator's wall clock instead of UTC.
Gitea push failed with TLS error during this session — code committed
locally, needs manual `./scripts/publish-to-gitea.sh --push` retry
when the desktop-11cj93j tunnel recovers.
- /auth/logout: delete all push_subscriptions rows for the user before
destroying the session. Best-effort with logger.warn on failure so an
operator can correlate any leaked-ring report to a real DB error.
- home.tsx handleLogout: call pushSub.disable() before the logout
mutation, raced against a 1.5s timeout so a stalled service worker
cannot block sign-out.
- executive-meeting-scheduler: switch the eligibility filter from
denylist (ne cancelled + ne completed) to whitelist
(eq status='scheduled'). Postponed / rescheduled / future statuses
can no longer trigger false 5-minute reminders.
- docker-compose.yml: pin TZ=Asia/Riyadh on postgres, api, and web
services so the scheduler's naive date/time math matches the
operator's wall clock instead of UTC.
Gitea push failed with TLS error during this session — code committed
locally, needs manual `./scripts/publish-to-gitea.sh --push` retry
when the desktop-11cj93j tunnel recovers.
Root causes the prior attempts missed:
1) Safari iOS/iPadOS ignores `interactive-widget=resizes-content`,
so `100dvh` never shrinks when the soft keyboard opens. Every
previous override that used `dvh` to size the dialog was sized
to the full screen and ran behind the keyboard.
2) The CSS rule from #622 (`[role="dialog"]:has(:focus)`) was
unconditional. On desktop, focusing any input inside a Radix
DialogContent instantly stretched it to width:100vw + top:0,
which collided with Radix's open-state slide-animation
transforms and rendered the dialog as a broken narrow strip
pinned in the top-left corner — visible in Tahani's laptop
screenshot when opening "Add Meeting".
3) `AdminFormDialog` puts `role="dialog"` on a NON-fixed inner
card, so the rule's `top/left/right` had no effect there
anyway; the card stayed centred in a layout-viewport-sized
wrapper which iOS does not shrink.
Fix:
- New `useVisualViewportVars` hook mounted in
NotificationsSocketBridge. Writes `--vv-height` and
`--vv-offset-top` on `<html>` from `window.visualViewport`,
updated on resize / scroll / orientationchange. Falls back
to `innerHeight`/0 if visualViewport is absent.
- Rewrote the index.css dialog rule:
* Gated inside `@media (any-pointer: coarse)` so desktop is
completely unaffected.
* Uses `var(--vv-height, 100vh)` and `var(--vv-offset-top, 0px)`
instead of `100dvh` / `0`, so dialogs actually track the
visible band on iOS.
- New `.dialog-vv-wrapper` helper class that applies the same
vv-sized top/height to bespoke full-screen overlay wrappers.
- Applied `.dialog-vv-wrapper` to AdminFormDialog and the admin
ConfirmDialog wrappers; switched their `fixed inset-0` to
`fixed inset-x-0 top-0 h-screen` and their inner cards from
`max-h-[90vh]`/`max-h-[85vh]` to `max-h-full` so the card
shrinks with the wrapper.
Kept `useScrollFocusedDialogField` unchanged — works correctly
once the dialog itself is sized to the visible band.
`tsc --noEmit` clean.
Root causes the prior attempts missed:
1) Safari iOS/iPadOS ignores `interactive-widget=resizes-content`,
so `100dvh` never shrinks when the soft keyboard opens. Every
previous override that used `dvh` to size the dialog was sized
to the full screen and ran behind the keyboard.
2) The CSS rule from #622 (`[role="dialog"]:has(:focus)`) was
unconditional. On desktop, focusing any input inside a Radix
DialogContent instantly stretched it to width:100vw + top:0,
which collided with Radix's open-state slide-animation
transforms and rendered the dialog as a broken narrow strip
pinned in the top-left corner — visible in Tahani's laptop
screenshot when opening "Add Meeting".
3) `AdminFormDialog` puts `role="dialog"` on a NON-fixed inner
card, so the rule's `top/left/right` had no effect there
anyway; the card stayed centred in a layout-viewport-sized
wrapper which iOS does not shrink.
Fix:
- New `useVisualViewportVars` hook mounted in
NotificationsSocketBridge. Writes `--vv-height` and
`--vv-offset-top` on `<html>` from `window.visualViewport`,
updated on resize / scroll / orientationchange. Falls back
to `innerHeight`/0 if visualViewport is absent.
- Rewrote the index.css dialog rule:
* Gated inside `@media (any-pointer: coarse)` so desktop is
completely unaffected.
* Uses `var(--vv-height, 100vh)` and `var(--vv-offset-top, 0px)`
instead of `100dvh` / `0`, so dialogs actually track the
visible band on iOS.
- New `.dialog-vv-wrapper` helper class that applies the same
vv-sized top/height to bespoke full-screen overlay wrappers.
- Applied `.dialog-vv-wrapper` to AdminFormDialog and the admin
ConfirmDialog wrappers; switched their `fixed inset-0` to
`fixed inset-x-0 top-0 h-screen` and their inner cards from
`max-h-[90vh]`/`max-h-[85vh]` to `max-h-full` so the card
shrinks with the wrapper.
Kept `useScrollFocusedDialogField` unchanged — works correctly
once the dialog itself is sized to the visible band.
`tsc --noEmit` clean.
#622's `useScrollFocusedDialogField` still failed on Tahani's iPad
for two reasons:
1) Touch-gate too narrow: `pointer: coarse` is *primary* pointer.
An iPad connected to a Magic Keyboard / trackpad reports primary
as `fine`, so the hook returned early and never armed. Broadened
to `(pointer: coarse) OR (any-pointer: coarse)` so any device
with any touch input qualifies.
2) `scrollIntoView` doesn't work inside `position: fixed` on iOS
Safari/PWA — the scroll silently no-ops. Replaced it with a
manual walk: from the focused field, ascend to the nearest
ancestor whose computed `overflow-y` is auto/scroll AND that
actually has overflow, stopping at the dialog root. Compute the
target's offset within that container's content and set
`container.scrollTop` so the target sits centred in the visible
band, clamped to [0, scrollHeight-clientHeight].
Also tightened cleanup per #622's reviewer note — timer IDs are
now removed from the tracking set inside the timeout callback as
well as on blur.
No CSS or dialog component changes. `tsc --noEmit` clean.
#622's `useScrollFocusedDialogField` still failed on Tahani's iPad
for two reasons:
1) Touch-gate too narrow: `pointer: coarse` is *primary* pointer.
An iPad connected to a Magic Keyboard / trackpad reports primary
as `fine`, so the hook returned early and never armed. Broadened
to `(pointer: coarse) OR (any-pointer: coarse)` so any device
with any touch input qualifies.
2) `scrollIntoView` doesn't work inside `position: fixed` on iOS
Safari/PWA — the scroll silently no-ops. Replaced it with a
manual walk: from the focused field, ascend to the nearest
ancestor whose computed `overflow-y` is auto/scroll AND that
actually has overflow, stopping at the dialog root. Compute the
target's offset within that container's content and set
`container.scrollTop` so the target sits centred in the visible
band, clamped to [0, scrollHeight-clientHeight].
Also tightened cleanup per #622's reviewer note — timer IDs are
now removed from the tracking set inside the timeout callback as
well as on blur.
No CSS or dialog component changes. `tsc --noEmit` clean.
The CSS `[role="dialog"]:has(:focus)` rule from #621 promotes any
focused dialog to a full-width sheet sized to `100dvh`, but Safari
does not auto-scroll the focused input into view inside the
dialog's own overflow container — it only tries to scroll the
document, which our position:fixed dialog ignores. The field stays
where it was, often behind the keyboard.
Added a global `useScrollFocusedDialogField` hook (in
artifacts/tx-os/src/hooks/) that listens to `document` focusin. If
the target is an input/textarea/select/contenteditable inside a
`closest('[role="dialog"]')`, it calls
`target.scrollIntoView({block:'center', behavior:'smooth'})` twice
(300ms and 600ms) to cover the iPad keyboard animation window and
the dialog's `max-height:100dvh` recalc. Timers are cleared on blur
and on unmount.
Mounted the hook once in `NotificationsSocketBridge` inside App.tsx
so it covers every page and every dialog kind we use (Radix
DialogContent, the bespoke AdminFormDialog, ad-hoc role="dialog"
wrappers) without touching their JSX.
No CSS or dialog component changes. `tsc --noEmit` clean.
The CSS `[role="dialog"]:has(:focus)` rule from #621 promotes any
focused dialog to a full-width sheet sized to `100dvh`, but Safari
does not auto-scroll the focused input into view inside the
dialog's own overflow container — it only tries to scroll the
document, which our position:fixed dialog ignores. The field stays
where it was, often behind the keyboard.
Added a global `useScrollFocusedDialogField` hook (in
artifacts/tx-os/src/hooks/) that listens to `document` focusin. If
the target is an input/textarea/select/contenteditable inside a
`closest('[role="dialog"]')`, it calls
`target.scrollIntoView({block:'center', behavior:'smooth'})` twice
(300ms and 600ms) to cover the iPad keyboard animation window and
the dialog's `max-height:100dvh` recalc. Timers are cleared on blur
and on unmount.
Mounted the hook once in `NotificationsSocketBridge` inside App.tsx
so it covers every page and every dialog kind we use (Radix
DialogContent, the bespoke AdminFormDialog, ad-hoc role="dialog"
wrappers) without touching their JSX.
No CSS or dialog component changes. `tsc --noEmit` clean.
The CSS `[role="dialog"]:has(:focus)` rule from #621 promotes any
focused dialog to a full-width sheet sized to `100dvh`, but Safari
does not auto-scroll the focused input into view inside the
dialog's own overflow container — it only tries to scroll the
document, which our position:fixed dialog ignores. The field stays
where it was, often behind the keyboard.
Added a global `useScrollFocusedDialogField` hook (in
artifacts/tx-os/src/hooks/) that listens to `document` focusin. If
the target is an input/textarea/select/contenteditable inside a
`closest('[role="dialog"]')`, it calls
`target.scrollIntoView({block:'center', behavior:'smooth'})` twice
(300ms and 600ms) to cover the iPad keyboard animation window and
the dialog's `max-height:100dvh` recalc. Timers are cleared on blur
and on unmount.
Mounted the hook once in `NotificationsSocketBridge` inside App.tsx
so it covers every page and every dialog kind we use (Radix
DialogContent, the bespoke AdminFormDialog, ad-hoc role="dialog"
wrappers) without touching their JSX.
No CSS or dialog component changes. `tsc --noEmit` clean.
The CSS `[role="dialog"]:has(:focus)` rule from #621 promotes any
focused dialog to a full-width sheet sized to `100dvh`, but Safari
does not auto-scroll the focused input into view inside the
dialog's own overflow container — it only tries to scroll the
document, which our position:fixed dialog ignores. The field stays
where it was, often behind the keyboard.
Added a global `useScrollFocusedDialogField` hook (in
artifacts/tx-os/src/hooks/) that listens to `document` focusin. If
the target is an input/textarea/select/contenteditable inside a
`closest('[role="dialog"]')`, it calls
`target.scrollIntoView({block:'center', behavior:'smooth'})` twice
(300ms and 600ms) to cover the iPad keyboard animation window and
the dialog's `max-height:100dvh` recalc. Timers are cleared on blur
and on unmount.
Mounted the hook once in `NotificationsSocketBridge` inside App.tsx
so it covers every page and every dialog kind we use (Radix
DialogContent, the bespoke AdminFormDialog, ad-hoc role="dialog"
wrappers) without touching their JSX.
No CSS or dialog component changes. `tsc --noEmit` clean.
The "edit role" dialog in admin.tsx was hand-rolled as a bespoke
<div role-less wrapper> with its own close button, sticky footer and
full-width split buttons. That made it look different from every
other admin dialog (add role, add/edit group, add user), and on
iPad the action buttons floated mid-dialog because the content
exceeded max-h-[92vh]. The custom wrapper also lacked role="dialog",
so the new `:has(:focus)` CSS keyboard rule (index.css:492) never
applied to it.
Replaced the wrapper with AdminFormDialog (maxWidth="lg", KeyRound
icon, same title key). All inner content — name/desc fields, rename
warning, permissions list, removal impact, RolePermissionHistory,
RecentActivityForTarget — moved verbatim as children. Removed the
custom sticky footer; AdminFormDialog provides the unified footer
with cancel/save buttons. Preserved testIds `edit-role-dialog` and
`edit-role-submit`. Disabled logic translated 1:1 into
submitDisabled + isPending props (note: `impactError` coerced to
boolean via `!!` to satisfy the prop type).
No behavior change: same handlers (closeEditDialog, handleEdit),
same loading spinner, same disabled conditions. Bonus: the keyboard
override CSS now applies because AdminFormDialog already sets
role="dialog".
`tsc --noEmit` clean.
The "edit role" dialog in admin.tsx was hand-rolled as a bespoke
<div role-less wrapper> with its own close button, sticky footer and
full-width split buttons. That made it look different from every
other admin dialog (add role, add/edit group, add user), and on
iPad the action buttons floated mid-dialog because the content
exceeded max-h-[92vh]. The custom wrapper also lacked role="dialog",
so the new `:has(:focus)` CSS keyboard rule (index.css:492) never
applied to it.
Replaced the wrapper with AdminFormDialog (maxWidth="lg", KeyRound
icon, same title key). All inner content — name/desc fields, rename
warning, permissions list, removal impact, RolePermissionHistory,
RecentActivityForTarget — moved verbatim as children. Removed the
custom sticky footer; AdminFormDialog provides the unified footer
with cancel/save buttons. Preserved testIds `edit-role-dialog` and
`edit-role-submit`. Disabled logic translated 1:1 into
submitDisabled + isPending props (note: `impactError` coerced to
boolean via `!!` to satisfy the prop type).
No behavior change: same handlers (closeEditDialog, handleEdit),
same loading spinner, same disabled conditions. Bonus: the keyboard
override CSS now applies because AdminFormDialog already sets
role="dialog".
`tsc --noEmit` clean.
Add a temporary debug overlay to the DialogContent component in `dialog.tsx` to display `keyboardInset`, `height`, and `offsetTop` values, and the keyboard open state.
Adjust dialog content to enable scrolling and center focused input fields, addressing issues on iOS and Android where content was hidden by the on-screen keyboard.
Adjusts drag activation constraints for pointer and touch sensors, enabling instant drag initiation in edit mode with a lower activation distance and no delay, while maintaining a higher threshold and delay outside of edit mode.
Adjusts drag activation constraints for pointer and touch sensors, enabling instant drag initiation in edit mode with a lower activation distance and no delay, while maintaining a higher threshold and delay outside of edit mode.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 1b66c87b-bec3-4f9e-b161-b4789c48a6fa
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/qrzw3bH
Replit-Helium-Checkpoint-Created: true
Modify the service reorder endpoint to handle cases where the client sends a partial list of service IDs, ensuring that unprovided services are appended to the end of the list without causing validation errors.
Modify the service reorder endpoint to handle cases where the client sends a partial list of service IDs, ensuring that unprovided services are appended to the end of the list without causing validation errors.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 4a518c00-92a6-443c-95de-44f6faa44d35
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/qrzw3bH
Replit-Helium-Checkpoint-Created: true
Modify dialog component to use window.innerWidth for width and position fixed, ensuring consistent display across iOS PWA environments by anchoring to the layout viewport.
Modify dialog component to use window.innerWidth for width and position fixed, ensuring consistent display across iOS PWA environments by anchoring to the layout viewport.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: b5415cc4-9462-49ea-bda1-5d40d587d3d1
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/qrzw3bH
Replit-Helium-Checkpoint-Created: true
Add `min-w-0` to `FormRow` component to prevent input fields from overlapping in RTL layouts and ensure proper column spanning on various screen sizes.