1e8f4435cdfc545ec189eee874d068007952d8e2
771 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
62e7be4c42 |
alert: keep meeting reminder buttons clickable while a Dialog is open
Task #478. When the upcoming-meeting reminder ("الاجتماع التالي") appeared on top of an open note thread dialog, the user could not tap "تأجيل" / "تم" / × / "تفاصيل". The buttons looked enabled and the popup sat above the dialog overlay (`z-[60]` over `z-50`), but nothing happened on click. Root cause: Radix `Dialog` is modal by default and its scroll-lock helper (`react-remove-scroll`) sets `pointer-events: none` on the document body while open. The meeting alert is portaled to body and inherited that, silently swallowing all clicks. Fix: in `artifacts/tx-os/src/components/executive-meetings/upcoming- meeting-alert.tsx`, add `pointer-events-auto` to the floating panel's className so it explicitly opts back in to receiving pointer events. Buttons inside inherit auto from the panel. Inline comment added referencing #478 and the Radix scroll-lock behavior. No change to the shared `Dialog` primitive, no other tokens or components touched. Existing `#282` behavior (hide alert while its own postpone modal is open) is preserved. Verified: - `tsc --noEmit` clean. - `notes-thread-dialog.spec.mjs` passes (no regression in the scenario that surfaced the bug). |
||
|
|
951fcce26b |
theme: lighten primary navy from #0B1E3F to #2C4A8A
Task #477. The brand primary color was a very dark navy (218 70% 15% ≈ #0B1E3F) and dominated buttons, the active sidebar row, the FAB, and focus rings. User asked to lighten it while keeping the same blue family. Change: in `artifacts/tx-os/src/index.css` (`:root`), bumped four tokens to `218 55% 32%` (≈ #2C4A8A) and updated the inline hex comment: - `--primary` - `--ring` - `--sidebar-primary` - `--sidebar-ring` White text on the new primary keeps WCAG AA contrast (≈5.3:1), so the existing `--primary-foreground: 0 0% 100%` is preserved. No other tokens (background, foreground, accent, secondary, destructive, borders) were touched. No component-level overrides were modified. |
||
|
|
76f2f7ce51 |
notes(ipad): keep dialog and inputs above the on-screen keyboard
Task #476. On iPad Safari, opening the keyboard while writing a new note or replying inside the note thread dialog covered the bottom of the dialog (textarea + send button). The dialog used `top:50%; translate(-50%,-50%)` against the layout viewport and never reacted to the visual viewport shrinking when the keyboard appeared. Changes: - artifacts/tx-os/index.html: added `interactive-widget=resizes-content` to the viewport meta so iOS 17+ resizes the layout viewport when the software keyboard opens. - artifacts/tx-os/src/hooks/use-visual-viewport.ts: new hook that subscribes to `window.visualViewport` `resize` / `scroll` / `orientationchange` (rAF-throttled) and exposes the visible height, the visual viewport `offsetTop`, and the bottom keyboard inset. - artifacts/tx-os/src/components/ui/dialog.tsx: `DialogContent` now reads the hook and, only when the keyboard inset exceeds ~80px, pins its center to `offsetTop + height/2` and caps `max-height` to the visible height. With the existing `translate(-50%,-50%)`, both edges stay inside the visible region above the keyboard. Desktop unaffected (inset is 0 → no inline style applied). - artifacts/tx-os/src/pages/notes.tsx: thread reply textarea and the top composer textarea now call `scrollIntoView({ block: "center" })` on focus and again whenever the keyboard inset changes while the field is focused. Both paths are gated on `keyboardInset > 80`, so desktop focus is untouched. Verification: - `tsc --noEmit` clean. - e2e: `notes-thread-dialog` and `notes-composer-color` both pass. |
||
|
|
db6e7726eb |
notes(ipad): keep dialog above the on-screen keyboard
Task #476. On iPad Safari, opening the keyboard while writing a new note or replying inside the note thread dialog covered the bottom of the dialog (textarea + send button). The dialog used `top:50%; translate(-50%,-50%)` against the layout viewport and never reacted to the visual viewport shrinking when the keyboard appeared. Changes: - artifacts/tx-os/index.html: added `interactive-widget=resizes-content` to the viewport meta so iOS 17+ resizes the layout viewport when the software keyboard opens. - artifacts/tx-os/src/hooks/use-visual-viewport.ts: new hook that subscribes to `window.visualViewport` `resize` / `scroll` / `orientationchange` (rAF-throttled) and exposes the visible height, the visual viewport `offsetTop`, and the bottom keyboard inset. - artifacts/tx-os/src/components/ui/dialog.tsx: `DialogContent` now reads the hook and, only when the keyboard inset exceeds ~80px (i.e. an actual on-screen keyboard, not browser-chrome jitter), pins its center to `offsetTop + height/2` and caps `max-height` to the visible height. With the dialog's existing `translate(-50%,-50%)`, both top and bottom edges stay inside the visible region above the keyboard. Desktop renders unchanged (inset is 0 → no inline style applied beyond what the caller passes). Deviation from plan: - Dropped the per-textarea `scrollIntoView` on focus (steps 4 of the plan). Reviewer flagged it as defensive/AI-shaped and a desktop regression risk; with the dialog now correctly capped to the visible viewport, the pinned-bottom reply / composer textareas are already fully on-screen, so the extra scroll isn't needed. Verification: - `tsc --noEmit` clean. - `notes-thread-dialog` e2e still passes. |
||
|
|
5dac662421 |
notes(ipad): keep dialog above the on-screen keyboard
Task #476. On iPad Safari, opening the keyboard while writing a new note or replying inside the note thread dialog covered the bottom of the dialog (textarea + send button). The dialog used `top:50%; translate(-50%,-50%)` against the layout viewport and never reacted to the visual viewport shrinking when the keyboard appeared. Changes: - artifacts/tx-os/index.html: added `interactive-widget=resizes-content` to the viewport meta so iOS 17+ resizes the layout viewport when the software keyboard opens. - artifacts/tx-os/src/hooks/use-visual-viewport.ts: new hook that subscribes to `window.visualViewport` `resize` / `scroll` / `orientationchange` events (rAF-throttled) and exposes the current visible height plus the bottom keyboard inset. - artifacts/tx-os/src/components/ui/dialog.tsx: `DialogContent` now reads the hook and, when the keyboard inset is non-trivial (> 80px), pins its center to the visible viewport mid-line and caps `max-height` to the visible height. Desktop renders unchanged (inset is 0 → no inline style applied beyond what the caller passes). - artifacts/tx-os/src/pages/notes.tsx: the thread dialog reply textarea and the top composer textarea now `scrollIntoView({ block: "center" })` on focus (after a 250ms delay so the keyboard animation settles), so the cursor lands inside the visible region on touch devices. Desktop focus is unaffected — `scrollIntoView` is a no-op when the element is already in the viewport. Verification: - `tsc --noEmit` clean. - Existing `notes-thread-dialog` e2e still passes (dialog stays capped at 85% viewport height, scroller still overflows, recipient chips and composer remain in viewport). |
||
|
|
36f6d1f5e6 |
notes(shared): show Send button on notes inside shared folders
User report (AR, RH on iPad): no Send button visible inside a shared "desk" folder. The shared-folder view was passing `hideSend` to every NoteCard for editors and rendered no card actions at all for read-only viewers, so nobody could send a note from a shared folder. Changes (artifacts/tx-os/src/pages/notes.tsx, SharedFolderView): - Editors: removed `hideSend` from the NoteCard so the existing Send affordance (and color/labels/archive/delete) appears as it does on the user's own notes. - Read-only viewers: added a small Send icon button to the static card (testid `shared-folder-note-send-<id>`) so they can also send the note via the existing send dialog. The send endpoint authorizes off the authenticated caller, so the note is sent under RH's identity (no server change needed). |
||
|
|
d0e907e409 |
notes(folders): allow drag-drop of notes onto shared folders (edit perm)
User report (AR): "shared folders also appear here — the user should
be able to move notes wherever they want." The folders rail already
lists shared-with-me folders, but only OWN folder rows registered as
@dnd-kit drop targets. Recipients with edit permission could see a
shared folder but couldn't drop a note onto it.
Changes:
- folders-rail.tsx: extend the inline `DroppableRow` to accept an
optional `ownerId`. Wrap each shared-folder row in a DroppableRow
when `sf.myPermission === "edit"`, passing the owning user's id;
read-only viewers stay non-droppable. The droppable id is namespaced
with `-shared-<ownerId>` so it never collides with the owner-side
row for the same folder id.
- notes.tsx: extend `sharedFolderBucketRef` to carry the bucket's
`ownerId` (sourced from `data.folder.ownerId`). In `handleDragEnd`,
read `o.ownerId` from the drop data:
• undefined → own folder / Unfiled. Allow only own notes.
• number → shared folder. Allow only when the source note
came from sharedBucket and `sharedBucket.ownerId
=== o.ownerId`.
This mirrors the server-side rule that an editor may move notes
only between folders owned by the same owner — cross-owner drops
are blocked client-side so we never round-trip a 400.
Out of scope:
- Cross-owner moves (would require a copy/share flow, not supported
by the server today).
- Drop targets on shared folders inside the FoldersBrowser tile grid
(the rail covers the user's reported screenshot; tiles can follow).
|
||
|
|
2629588f32 |
notes(folders): allow drag-drop of notes onto shared folders (edit perm)
User report (AR): "shared folders also appear here — the user should
be able to move notes wherever they want." The folders rail already
lists shared-with-me folders, but only OWN folder rows registered as
@dnd-kit drop targets. Recipients with edit permission could see a
shared folder but couldn't drop a note onto it.
Changes:
- folders-rail.tsx: extend the inline `DroppableRow` to accept an
optional `ownerId`. Wrap each shared-folder row in a DroppableRow
when `sf.myPermission === "edit"`, passing the owning user's id;
read-only viewers stay non-droppable. The droppable id is namespaced
with `-shared-<ownerId>` so it never collides with the owner-side
row for the same folder id.
- notes.tsx: extend `sharedFolderBucketRef` to carry the bucket's
`ownerId` (sourced from `data.folder.ownerId`). In `handleDragEnd`,
read `o.ownerId` from the drop data:
• undefined → own folder / Unfiled. Allow only own notes.
• number → shared folder. Allow only when the source note
came from sharedBucket and `sharedBucket.ownerId
=== o.ownerId`.
This mirrors the server-side rule that an editor may move notes
only between folders owned by the same owner — cross-owner drops
are blocked client-side so we never round-trip a 400.
Out of scope:
- Cross-owner moves (would require a copy/share flow, not supported
by the server today).
- Drop targets on shared folders inside the FoldersBrowser tile grid
(the rail covers the user's reported screenshot; tiles can follow).
|
||
|
|
26ed7a195e |
notes(sent): multi-select + bulk delete on Sent tab (task #472)
- Add useBulkDeleteSentNotes hook with bounded concurrency (worker
pool, cap = 6) over the existing per-id DELETE /notes/:id endpoint;
returns {ok, failed} so the UI can render a precise toast. One cache
invalidation at the end (notes + folders).
- notes.tsx: page-level selection state for the Sent tab only
(sentSelectionMode, selectedSentIds, bulkDeleteOpen). Header
Select/Cancel toggle, sticky bulk-action bar with count, select-all/
clear, in-bar Cancel, Delete, and an AlertDialog confirmation. Auto
exits selection mode when leaving the Sent tab; prunes selected ids
to currently visible filteredSent on every change (search, refresh,
successful delete).
- SentList: switched the outer card from a nested <button> to a
div role="button" tabIndex=0 with Enter/Space handler so the
selection-mode Checkbox is no longer a nested interactive control.
Checkbox is aria-hidden / pointer-events-none inside selection mode.
- i18n: notes.bulk* keys (Select / Cancel / SelectAll / Clear / Count
with _one plural / Delete / Confirm title+body / Result success /
partial / failure) in both en.json and ar.json.
- New e2e: tests/notes-sent-bulk-delete.spec.mjs seeds 3 sent notes,
selects 2, confirms the bulk delete, asserts the cards are gone and
the surviving note remains (DB + UI).
- Out of scope (proposed as follow-ups #473/#474/#475): undo on bulk
delete, multi-select on Inbox/Archived tabs, additional e2e cases.
|
||
|
|
d7a386cf75 |
notes: bulk-select + bulk-delete on Sent tab (task #472)
- Add `useBulkDeleteSentNotes` hook that fans out parallel DELETE
/notes/:id calls via Promise.allSettled and returns {ok, failed}
so the UI can render a precise toast on partial failure. Caches
invalidated once via `invalidateNotesAndFolders`.
- NotesPage: page-level `sentSelectionMode` + `selectedSentIds`
Set, auto-reset when navigating away from the Sent tab.
- Header gets a Select / Cancel toggle, gated on
`view === "sent" && filteredSent.length > 0`.
- New sticky bulk-action bar above the sent list with count,
select-all/clear toggle, and Delete button.
- SentList accepts optional selectionMode/selectedIds/onToggle
props; cards swap onClick (open → toggle) and show a Checkbox
overlay + rose ring when checked.
- AlertDialog confirms before deleting; toast reports all-success,
partial, or all-failed using pluralized i18n keys.
- New i18n keys under `notes.bulk*` in ar.json + en.json.
No server changes (existing DELETE /notes/:id is reused).
Out of scope (per task spec): undo, bulk-archive, bulk endpoint,
multi-select on Active/Received/Archived tabs.
|
||
|
|
019e10fa4a |
Task #471: Make the incoming-note popup a single uniform color
The popup card was rendering in three visible shades of the note's color: a darker header band, the note color in the body, and another darker action-bar band — caused by `bg-black/5 dark:bg-white/5` overlays plus dividers on the header and footer rows. The user wanted the entire card to be a single flat color matching the note. Changes (artifacts/tx-os/src/components/notes/incoming-note-popup.tsx): - Drag handle / header row: removed `border-b border-black/10 dark:border-white/10 bg-black/5 dark:bg-white/5`. The row is now fully transparent and inherits the outer card's `colorBg(noteColor)`. - Action row: removed `border-t border-black/10 dark:border-white/10 bg-black/5 dark:bg-white/5`. Same treatment — fully transparent, no divider line. - The outer card div remains the only source of background color, so every region now shows the exact same shade in both light and dark mode. Out of scope (untouched): avatar bg/ring, ping pulse, default-color behavior (`bg-background` still kicks in for the default color), animation, drag/pointer handlers, button layout. Validation: - `pnpm --filter @workspace/tx-os exec tsc --noEmit` passes. - Pre-existing `test` workflow failures (executive-meetings PDF, notes-share, groups-crud) are unchanged and out of scope. |
||
|
|
05eeb177ff |
Add a plus icon to indicate creating a new note
Update the notes composer component to include a Plus icon next to the "Take a note..." placeholder, visually indicating the action to create a new note. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 01f3a5db-a2fa-45cb-9733-81a92ebf3216 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/mkNwf1z Replit-Helium-Checkpoint-Created: true |
||
|
|
6a541b5a21 |
Remove the outer color ring from incoming notes and square the corners
Update incoming note popup to remove the outer color ring and square its corners, aligning with user request for a cleaner visual appearance. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 99be2532-d204-4370-a1aa-cfc59e3abd4a Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/mkNwf1z Replit-Helium-Checkpoint-Created: true |
||
|
|
671cedf51d |
Task #470: Remove popup color ring and square its corners
User asked to drop the colored ring around the floating new-note popup (added in #468) and replace the rounded corners with square ones for a cleaner, more formal notification look. Changes (artifacts/tx-os/src/components/notes/incoming-note-popup.tsx): - Removed `ring-4 ${ringClass}` from the outer card div so no colored outline appears around the popup. - Replaced `rounded-2xl` with `rounded-none` so the card has square 90° corners on all sides. `overflow-hidden` is preserved so the inner header / body / action bar still clip cleanly to the rectangle. - Dropped the now-unused `colorRingStrong` import and its derived `ringClass` local. The shared helper itself stays in `notes-api.ts` in case future surfaces want it. - Kept the neutral `shadow-2xl` so the card still separates from the background, and kept the avatar / ping color tinting from #468. Validation: - `pnpm --filter @workspace/tx-os exec tsc --noEmit` passes. - No other functional changes (drag, dismiss, mark read, reply, open, checklist toggle, queue counter, RTL all untouched). - Pre-existing `test` workflow failures (executive-meetings PDF, notes-share, groups-crud) are unchanged and out of scope. |
||
|
|
aaa705d843 |
Task #468: Match incoming-note popup colors to the note's own color
User reported that the floating "new note" popup had a fixed amber/
orange ring, avatar ring, ping pulse and shadow tint, which clashed
visually whenever the underlying note used a different color (e.g. a
pink note appeared inside an orange popup).
Changes:
- artifacts/tx-os/src/lib/notes-api.ts:
- Extended the NOTE_COLORS array with four new fields per color:
`ringStrong` (popup outer ring), `avatarBg` (avatar circle bg+text),
`avatarRing` (2px avatar ring), and `ping` (animated pulse).
- Each entry uses literal Tailwind utility strings so v4's build-time
scanner picks them up — same pattern already used for `accent-*`.
- Default + gray colors map to neutral slate variants so they stay
legible on the white card surface.
- Added small helpers: `colorRingStrong`, `colorAvatarBg`,
`colorAvatarRing`, `colorPing` (each falls back to the default
entry for unknown ids, matching `colorAccent`'s behavior).
- artifacts/tx-os/src/components/notes/incoming-note-popup.tsx:
- Imported the new helpers and resolved them once per render from
`current.color` (works for both note and reply popup variants since
both expose the same `color` field on the payload).
- Replaced every `amber-*` class on the outer ring, the avatar
fallback bg/text, the avatar image ring, and the animated ping
with the resolved color classes.
- Dropped the colored shadow tint (`shadow-amber-500/30`) — the
card now uses a neutral `shadow-2xl` so the only colored chrome
is the ring matching the note.
Validation:
- `pnpm --filter @workspace/tx-os exec tsc --noEmit` passes.
- No remaining `amber` literal in the popup file (only an
explanatory comment mentions the word).
- Pre-existing `test` workflow failures (executive-meetings PDF,
notes-share, groups-crud) are unchanged and out of scope.
|
||
|
|
b0647cb9b8 |
Task #466: Clean notes page from email-like 4-tab bar
User asked to remove the prominent 4-tab bar (My Notes / Inbox / Sent / Archive) from the notes page because it made the page feel like an email client, and they rarely use the share-between-users feature. Changes (artifacts/tx-os/src/pages/notes.tsx): - Deleted the inline-flex TabButton row that held the four view tabs. - Page now opens directly on the unified "active" feed. - Added an overflow DropdownMenu (⋯ MoreVertical icon) on the end of the controls row containing three items: Inbox (with unread badge), Sent, Archived. Each item calls setView() with the same TabId values, so all downstream branching (data fetching, filtering, composer visibility, folder rail) is unchanged. - Unread inbox badge appears only inside the dropdown next to the Inbox item (the existing `data-testid="notes-inbox-unread-badge"` is preserved). The overflow trigger itself stays visually clean per the task requirement. - Added a small "← My Notes" back button (testid notes-back-to-active) that appears only when view !== "active" so the user can return after drilling into Inbox/Sent/Archived without a visible tab bar. - Removed the now-unused TabButton component definition. Tests (artifacts/tx-os/tests/notes-inbox.spec.mjs): - Replaced the two notes-tab-received / notes-tab-sent clicks with the open-dropdown-then-select-item sequence using the new overflow testids. Validation: - pnpm tsc --noEmit passes for @workspace/tx-os. - Architect review: no severe issues; layout, a11y, RTL, z-index, and state-reset behavior all noted as sound. - The `test` workflow has pre-existing failures (executive-meetings PDF, notes-share, groups-crud) that are explicitly out of scope per the task description and tracked by other open follow-up tasks. No backend, schema, API, or i18n JSON changes were needed: the file uses inline t() defaults, and `notes.tabs.received/sent/archived` keys are reused inside the dropdown. |
||
|
|
ef1f23a11d |
Task #466: Clean notes page from email-like 4-tab bar
User asked to remove the prominent 4-tab bar (My Notes / Inbox / Sent / Archive) from the notes page because it made the page feel like an email client, and they rarely use the share-between-users feature. Changes (artifacts/tx-os/src/pages/notes.tsx): - Deleted the inline-flex TabButton row that held the four view tabs. - Page now opens directly on the unified "active" feed. - Added an overflow DropdownMenu (⋯ MoreVertical icon) on the end of the controls row containing three items: Inbox (with unread badge), Sent, Archived. Each item calls setView() with the same TabId values, so all downstream branching (data fetching, filtering, composer visibility, folder rail) is unchanged. - Unread inbox badge now appears (a) as a small numeric dot on the overflow trigger when not currently in Inbox view, and (b) inside the dropdown next to the Inbox item — the existing `data-testid="notes-inbox-unread-badge"` is preserved. - Added a small "← My Notes" back button (testid notes-back-to-active) that appears only when view !== "active" so the user can return after drilling into Inbox/Sent/Archived without a visible tab bar. - Removed the now-unused TabButton component definition. Tests (artifacts/tx-os/tests/notes-inbox.spec.mjs): - Replaced the two notes-tab-received / notes-tab-sent clicks with the open-dropdown-then-select-item sequence using the new overflow testids. Validation: - pnpm tsc --noEmit passes for @workspace/tx-os. - Architect review: no severe issues; layout, a11y, RTL, z-index, and state-reset behavior all noted as sound. - The `test` workflow has pre-existing failures (executive-meetings PDF, notes-share, groups-crud) that are explicitly out of scope per the task description and tracked by other open follow-up tasks. No backend, schema, API, or i18n JSON changes were needed: the file uses inline t() defaults, and `notes.tabs.received/sent/archived` keys are reused inside the dropdown. |
||
|
|
f937362868 |
Task #463: @dnd-kit notes drag + reorder
- Replace HTML5+touch drag with @dnd-kit; MouseSensor (desktop, 8px) + TouchSensor (iPad, 200ms long-press) so input sources never overlap. - Add sort_order column; ORDER BY asc(sortOrder), updatedAt desc. - PATCH /notes/reorder: strict isPinned boolean check, bucket+permission scoped, all writes wrapped in db.transaction for atomicity. - PATCH /notes/:id stamps sort_order = min-1 on folder/pin bucket change. - Client useReorderNotes (PATCH) with optimistic cache update. - handleDragEnd builds reorder payload from FULL bucket (owner notes or shared-folder bucket via ref), not the filtered subset, so hidden siblings under search/label filters keep their order. - Drag guarded for view-only contexts: source must be owned OR in editable shared bucket; folder-drop additionally requires isOwn. - SharedFolderView publishes its data.notes via bucketRef when viewer has edit permission, enabling correct reorder in shared folders. - Layout fix at narrow viewport: rail stacks above notes (flex-col md:flex-row) so iPad portrait drag has proper bbox. - Playwright tests: notes-folders.spec.mjs both desktop pointer drag and touch long-press drag pass (33s). - OpenAPI codegen skipped: notes-api.ts is hand-written. - Out of scope (pre-existing failures): executive-meetings reorder/font, notes-share PATCH 403/404, groups-crud rollback. |
||
|
|
daa4f6c038 |
Task #463: @dnd-kit notes drag + reorder
- Replace HTML5+touch drag with @dnd-kit (PointerSensor distance:8, TouchSensor delay:200/tol:8) matching home.tsx pattern. - Add sort_order column to notes; ORDER BY asc(sortOrder), updatedAt desc. - New PATCH /notes/reorder endpoint: strict isPinned boolean validation, bucket+permission scoped, all writes in db.transaction for atomicity. - PATCH /notes/:id stamps sort_order = min-1 on folder/pin bucket change. - Client useReorderNotes hook with optimistic cache update. - handleDragEnd builds reorder payload from FULL bucket (owner notes or shared-folder bucket via ref), not the filtered/search subset, so hidden siblings retain stable order. - SharedFolderView publishes its data.notes via bucketRef when viewer has edit permission, enabling correct reorder in shared folders. - Layout fix at narrow viewport: rail stacks above notes (flex-col md:flex-row) so iPad portrait drag has proper bbox. - Playwright tests: notes-folders.spec.mjs both desktop pointer drag and touch long-press drag pass (32s). - OpenAPI codegen skipped: notes-api.ts is hand-written. - Out of scope (pre-existing failures): executive-meetings reorder/font, notes-share PATCH 403/404, groups-crud rollback. |
||
|
|
e103cbf5c1 |
Task #462: Fix iPad notification sounds (second attempt)
Original task: After #461 added an HTMLAudioElement playback path for iOS, users still reported no sound on iPad. #462 identifies and fixes three bugs in that earlier attempt. Root causes addressed: 1. iOS unlocks ONE element per gesture, not all 8. The previous loop over 8 separate Audio elements left 7 of them gesture-locked. 2. `crossOrigin = "anonymous"` flipped same-origin .wav requests into CORS mode, causing silent load failure in Safari. 3. `el.volume = 0` is read-only on iOS, so the "silent priming" idea in #461 would have played 8 real chimes at once on first tap. Implementation: - Replaced the 8-element Map with a SINGLE shared HTMLAudioElement on iOS; rotate `.src` per play (iOS unlock is element-bound, not URL). - Added `SILENT_WAV_DATA_URL`: a 60-byte inline silent WAV used to prime the element in `unlock()` — no network, no audible output. - Removed `crossOrigin = "anonymous"`. - `testPlay()` on iOS now calls `play()` directly (skipping the silent-prime) so the gesture-bound `play()` lands on the actual sound. Non-iOS keeps the original `unlock()`-then-`play()` order. - `unlock()` is a no-op once primed; deliberately does NOT pause/reset the silent clip in its resolve handler — letting the ~30ms silence end naturally avoids racing with a `playIos()` `src` swap from a click that fires immediately after pointerdown. Public API unchanged: play / testPlay / unlock / isUnlocked / AUTOPLAY_BLOCKED_EVENT all preserve their signatures and observable behavior. Test globals (__txosNotifPlayCount, __txosNotifLastSound, __txosNotifCtxState) preserved. Locale strings (`notifSettings.autoplayHintIos`) already focused on media volume from #461 — no change needed. Validation: `tsc --noEmit` clean. The pre-existing failures in the api-server / test workflow (executive-meetings.ts) are unrelated and explicitly out of scope per the plan. Manual iPad verification is required to confirm the fix in production. Files changed: - artifacts/tx-os/src/lib/notification-sounds.ts (rewritten) |
||
|
|
fa529cd95a |
Improve sound playback on iOS by priming audio elements
Refactor the `NotificationPlayer` class to correctly handle audio playback initiation on iOS, ensuring that sounds play reliably after user gestures by properly managing the unlocked state based on successful priming of HTMLAudioElements. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: f541a3b8-ce77-4f70-9fa4-f1272f6e5c7a Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/mkNwf1z Replit-Helium-Checkpoint-Created: true |
||
|
|
ec29657bf5 |
Task #461: Fix notification sounds on iPad (route via media channel)
Problem: User reported zero sound on iPad — even the "Test sound" button in notification settings produced silence. YouTube and other videos worked fine on the same iPad. Root cause: The app's notification player used the Web Audio API (AudioContext + BufferSource). On iOS Safari (iPad/iPhone), Web Audio plays through the **ringer channel**, which is silenced by: - The hardware silent switch (older iPads), - Control Center's mute icon, - Or simply having ringer volume at 0 (independent of media volume). HTMLAudioElement (the API behind <video> and YouTube) plays through the **media channel**, which is what users actually have turned up. Fix in artifacts/tx-os/src/lib/notification-sounds.ts: - Added an HTMLAudioElement playback engine alongside the existing Web Audio engine. Engine selection is one-time per session via the existing `detectIos()` helper (handles iPadOS-as-Mac masquerade). - iOS path: lazy `new Audio(url)` per sound id with preload="auto", primed in `unlock()` by play()-then-pause() at volume 0 inside the user gesture so each element is permanently allowed to play later out-of-gesture (iOS requirement). Real `play()` resets currentTime and plays at full volume; rejection of the play promise dispatches the existing autoplay-blocked hint. - Non-iOS path: unchanged Web Audio behavior preserved verbatim. - All public API preserved: `unlock()`, `play()`, `testPlay()`, `isUnlocked()`, `AUTOPLAY_BLOCKED_EVENT`. No changes needed to use-audio-unlock, use-autoplay-hint, use-notifications-socket, or notification-settings. - Throttling (3s burst-coalesce), bypass for testPlay, vibration, test-only observability globals, and isIos hint flag all preserved. - `__txosNotifCtxState` test hook now reports "running"/"suspended" on iOS based on `unlocked` flag (no AudioContext exists there). Locale updates (ar.json + en.json): - `notifSettings.autoplayHintIos` rewritten: now tells users to raise the **media** volume (same one that controls YouTube) instead of the old "turn off Silent Mute" advice — matches the new playback channel. Verification: tsc --noEmit clean. Pre-existing `test` workflow failure on unrelated executive-meetings.ts type errors is out of scope. Out of scope per task: no PWA, no push notifications, no settings UI redesign, no new sound files. Manual iPad QA needed before merging. |
||
|
|
4650014631 |
Task #460: Hide iPad Safari URL bar on short pages too
Followup to #459. After that fix, Notes/Home no longer had overflow-hidden, but the user reported the URL bar still didn't collapse on Notes (when few notes), Services, or Notifications. Root cause: iOS Safari only collapses its URL bar when the document is actually scrollable. Services (small grid), Notifications (short list), and Notes-when-empty all fit within the viewport on iPad, so there's no scroll trigger. Executive Meetings worked only because its schedule list always overflows. Fix (touch-only, single CSS rule in index.css @layer base): @media (hover: none) and (pointer: coarse) { html, body { min-height: calc(100svh + 1px); } } Why 100svh: it's the viewport-with-URL-bar-visible height, so the +1px gap stays constant regardless of whether the bar is shown or hidden — no oscillation. Why touch-only media query: keeps desktop browsers from showing a stray scrollbar on short pages. Universal fix — applies to every page automatically, no per-page edits. Out of scope (per task): no PWA install meta, no per-page changes, no styling changes. Pre-existing `test` workflow failure on unrelated executive-meetings.ts type errors is not addressed here. |
||
|
|
920d224a68 |
Task #459: Hide iPad Safari URL bar across all apps (notes + home)
Problem: On iPad Safari, the executive-meetings app collapsed the URL bar on scroll (native-app feel), but Notes and Home kept it visible permanently because their root containers were not document-scrollable. - notes.tsx used `h-screen ... overflow-hidden` with an internal `flex-1 min-h-0 overflow-y-auto` scroll region. - home.tsx used `min-h-screen ... overflow-hidden`. With no document-level scroll, iOS Safari has no trigger to hide the URL bar. Fix (quick-fix path the user picked, no PWA install meta): - notes.tsx: root → `min-h-[100dvh] os-bg flex flex-col` (removed `overflow-hidden`, swapped `h-screen` → `min-h-[100dvh]`). Header now `sticky top-0 z-10` so it stays pinned. Inner body wrapper changed from `flex-1 min-h-0 overflow-y-auto flex flex-col` to `flex-1 flex flex-col` so the document (not an inner div) scrolls — giving Safari the trigger it needs. - home.tsx: root → `min-h-[100dvh] os-bg flex flex-col relative` (removed `overflow-hidden`). Why `100dvh`: dynamic viewport height sizes against the *visible* area, so the layout reflows cleanly when the URL bar appears/hides. Out of scope (matches task): no PWA meta tags, no other pages touched (spot-checked others: chat/services/orders/notifications/admin already use min-h-screen without overflow-hidden), no styling changes, executive- meetings page untouched (it's the reference). Verification: tsc --noEmit clean. Pre-existing test workflow failure on unrelated executive-meetings.ts type errors is out of scope. |
||
|
|
dba168b299 |
notes(#457): enlarge notes page header bar
- Outer header padding: px-4 py-3 → px-6 py-5 for a noticeably
taller bar.
- Title row gap-3 → gap-4; back button p-2 → p-2.5 with size 24 icon
(was 20).
- Title "Notes" text-lg → text-2xl; StickyNote icon size 20 → 28;
inner gap-2 → gap-3.
- Search:
- container max-w-md → max-w-xl, min-w-[180px] → min-w-[220px]
- Input gets h-12 text-base + ps-11
- Search icon size 16 → 20, offset bumped from 10 → 14 to match
the new padding (RTL + LTR).
- Labels button: removed size="sm", now h-12 px-4 text-base with
Tag icon size 20 (was 16) + me-2 (was me-1) for the larger label.
- Tab row spacing: mt-3 gap-2 → mt-4 gap-3; tab container padding
p-1 → p-1.5.
- Tab icons size 14 → 18 with me-2 (was me-1).
- Inbox unread badge: text-[10px] / 18×18 → text-xs / 22×22 with
px-1.5 so two-digit counts still fit.
- TabButton: px-3 py-1 text-sm → px-4 py-2 text-base.
- Header still uses flex-wrap so it folds gracefully on narrow
screens; works in both RTL and LTR (search icon position uses
the existing isRtl conditional).
- Out of scope (per task): no color/glass-panel changes, no behavior
changes, no enlargement of the body content under the header,
and no changes to the SharedFolderView header.
- Pre-existing executive-meetings.ts type errors unchanged.
|
||
|
|
4e00a20bda |
notes(#454): per-recipient view/edit folder sharing
- Schema: noteFolderShares.permission ('view'|'edit', default 'view').
Project uses drizzle-kit push (no migration files); existing rows
pick up the default automatically on next push.
- Server: resolveFolderEditAccess + emitFolderChanged helpers.
- POST/PATCH/DELETE/checklist now allow folder editors.
- Editors stamp notes with folder owner's userId; labels validated
against owner; PATCH editors blocked from unfile (folderId=null)
and cross-folder moves to non-matching owners.
- emitFolderChanged fires for owner AND editor mutations across
create / patch (old + new folder) / delete / checklist toggle.
- PUT /shares accepts both legacy recipientUserIds and new
recipients:[{userId,permission}]; diffs add/update/remove/perm-flip.
- GET /shares, /shared-with-me, /shared-notes return permission /
myPermission / readOnly.
- Distinct socket events: note-folder-shared (added),
note-folder-share-updated (permission flipped),
note-folder-unshared (removed). Permission events carry the new
permission so clients can react.
- Client:
- notes-api types: FolderSharePermission, myPermission on
SharedFolder/SharedFolderView, permission on FolderShareRecipient,
readOnly:boolean on SharedFolderNote.
- useUpdateFolderShares takes recipients[].
- useCreate/Update/DeleteNote also invalidate ['note-folders'] so
actor's shared-folder rail badge stays fresh.
- FolderShareDialog: per-row checkbox + segmented View/Edit toggle.
- SharedFolderView: editor mode mounts Composer + NoteCard with
full edit/delete/archive affordances; Send hidden in editor
context (Composer + NoteCard hideSend prop) since /send is
owner-only.
- folders-rail: per-folder permission badge.
- socket: handlers for shared / share-updated / unshared all
invalidate the right query keys for live UI flips.
- Locales: shareDescriptionPerm, permissionView/Edit, canEdit
(en + ar).
- Tests: new permission roundtrip test in notes-share.test.mjs covering
view-rejects-write, edit-can-write, editor-cannot-unfile,
editor-create-stamped-to-owner, downgrade/upgrade flips.
- Pre-existing executive-meetings.ts type errors are out of scope.
|
||
|
|
1aad708cb7 |
notes(#454): per-recipient view/edit folder sharing
- Schema: noteFolderShares.permission ('view'|'edit', default 'view').
- Server: resolveFolderEditAccess + emitFolderChanged helpers.
- POST/PATCH/DELETE/checklist now allow folder editors.
- Editors stamp notes with folder owner's userId; labels validated
against owner; PATCH editors blocked from unfile (folderId=null)
and cross-folder moves to non-matching owners.
- emitFolderChanged fires for owner AND editor mutations across
create / patch (old + new folder) / delete / checklist toggle.
- PUT /shares accepts both legacy recipientUserIds and new
recipients:[{userId,permission}]; diffs add/update/remove/perm-flip.
- GET /shares, /shared-with-me, /shared-notes return permission /
myPermission / readOnly.
- Client:
- notes-api types: FolderSharePermission, myPermission on
SharedFolder/SharedFolderView, permission on FolderShareRecipient,
readOnly:boolean on SharedFolderNote.
- useUpdateFolderShares takes recipients[].
- useCreate/Update/DeleteNote also invalidate ['note-folders'] so
actor's shared-folder rail badge stays fresh.
- FolderShareDialog: per-row checkbox + segmented View/Edit toggle.
- SharedFolderView: editor mode mounts Composer + NoteCard with
full edit/delete/archive affordances; Send hidden in editor
context (Composer + NoteCard hideSend prop) since /send is
owner-only.
- folders-rail: per-folder permission badge.
- socket: note-folder-shared also invalidates ['notes'] +
['note-folders'] for fanout to owner / other editors.
- Locales: shareDescriptionPerm, permissionView/Edit, canEdit
(en + ar).
- Pre-existing executive-meetings.ts type errors are out of scope.
|
||
|
|
0e384b6898 |
Improve how shared folders appear in the navigation sidebar
Hide the "Shared with me" header in certain views and add tooltips to indicate folder owners. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 26650ab1-4e19-4da3-8ed7-e3535055e62a Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/R0O2qN7 Replit-Helium-Checkpoint-Created: true |
||
|
|
5868507bcc |
Notes: show "Shared with me" folders on the default screen too
Task #453. Recipient (e.g. yas) couldn't see folders that an owner (e.g. admin) had shared with them — the server returned them correctly via /note-folders/shared-with-me, but the rail's "Shared with me" section was gated on `mode === "rail"`. On the default Notes screen (My Notes tab + "All notes" selection) the rail is mounted in `chips-only` mode, so the section was never rendered. Most recipients only ever saw the default screen, so shared folders were effectively invisible. Fix: drop the `mode === "rail"` gate in folders-rail.tsx so the shared-with-me section renders in both modes. The section already uses `contents md:block` (so chips inline into the mobile horizontal strip), and its "Shared with me" label is already `hidden md:block`, so the rendering stays compact on mobile and labelled on desktop in both modes. No backend changes; the existing socket invalidations on `note-folder-shared` / `note-folder-unshared` already keep the list fresh in realtime. Out of scope: - FoldersBrowser (the grid/list of own folders in the main content area) intentionally not extended to also list shared folders; the rail entry is sufficient and matches the task's "as a chip alongside the own-folder chips" outcome on mobile and "Shared with me" sidebar section on desktop. - No changes to permissions, notifications, or per-note sharing. - Notes top-bar freeze (Task #452) is untouched. |
||
|
|
5753aa176d |
Notes: lift top bar out of the scroll area so it never scrolls off
Task #452. After #451, the page wrapper was h-screen + overflow-y-auto with the header still using `sticky top-0 z-20`. Users kept reporting that the bar slides up out of view on scroll — the well-known `position: sticky` failure mode when the sticky child is a direct flex item of the same element that is also the scroll container. Restructured artifacts/tx-os/src/pages/notes.tsx so the bar is a sibling of the scroller, not inside it: - Page wrapper: `h-screen os-bg flex flex-col overflow-hidden` (was `... overflow-y-auto`). The wrapper itself never scrolls. - Header div: dropped `sticky top-0 z-20`, added `shrink-0`. As a non-shrinking flex child of the column wrapper it stays pinned at the top by layout, with no sticky math involved. - Wrapped the composer block and the main content row (`flex-1 px-4 py-6 max-w-6xl ... flex gap-6` plus its inner `flex-1 min-w-0` column) inside a single new scrolling region: `<div className="flex-1 min-h-0 overflow-y-auto flex flex-col">`. `min-h-0` is required so the flex child can shrink below its content height and let `overflow-y-auto` actually engage. - Dialogs (Edit, Labels, Send, FolderShare, Thread) remain siblings of the scroller, so popovers/modals are unaffected. No changes to index.css, header styling, composer styling, folder layout, or any other page. RTL/LTR behavior unchanged. Out of scope: pre-existing test failures (executive-meetings, notes-share, service-orders) are unrelated to this layout fix. |
||
|
|
5f7bb77985 |
Task #451: pin Notes header via an internal scroll container
Despite Tasks #448–#450 (sticky header + fixing .os-bg / html,body overflow rules), the user kept reporting the bar disappears on scroll. position: sticky is sensitive — the browser was picking the wrong scroll container in this layout. Robust fix: stop relying on document/body for scrolling on the Notes page. Changed the page wrapper className in artifacts/tx-os/src/pages/notes.tsx from min-h-screen os-bg flex flex-col to h-screen os-bg flex flex-col overflow-y-auto Now the Notes wrapper is a known scroll container that's exactly one viewport tall. The sticky header inside it pins reliably to the top edge of that container (== top of the viewport). Composer flush-placement (Task #448), index.css overflow-x:clip on html,body (Task #450), and other pages using .os-bg are all untouched. Single-line change, no other refactors. |
||
|
|
a8eeae56fd |
Task #450: switch html/body horizontal clip from hidden to clip
After Task #449 the sticky Notes header was still scrolling away. Root cause: `overflow-x: hidden` on `html, body` makes both elements scroll containers. Body sized itself to its content (no internal scroll happens), so a `position: sticky` descendant got scoped to a non-scrolling container and rode off-screen with the page. `overflow-x: clip` clips overflowing content the same visual way `hidden` does, but it does NOT establish a scroll container, so sticky descendants continue to resolve against the viewport and pin correctly. Changed only `artifacts/tx-os/src/index.css` (the `html, body` rule added in #449). All other rules untouched. `overflow: clip` is supported in Chrome 90+, Firefox 81+, Safari 16+. |
||
|
|
09cc87d510 |
Task #449: make Notes header truly stick by fixing scroll-context bug
After Task #448 switched the Notes header to `sticky top-0`, it still scrolled away with the page. Root cause was in `artifacts/tx-os/src/index.css`: the shared `.os-bg` class set `overflow-x: hidden`, which per CSS spec turns the element into a scroll container. Since the Notes page wrapper carries `os-bg` and also has `min-h-screen`, the wrapper expanded to fit content (never scrolled internally) while the document body scrolled — which meant the sticky header was scoped to a non-scrolling container and rode along with the page. Fix: - Removed `overflow-x: hidden` from `.os-bg`. - Added `html, body { overflow-x: hidden; }` inside the existing `@layer base` block so the horizontal-clip protection still exists at the document level (which does not break sticky in descendants). No other changes. Notes page header now actually pins to the top during scroll across all tabs / folder views, and other pages that use `.os-bg` (home, services, executive-meetings, etc.) keep their horizontal-overflow protection via the body-level rule. |
||
|
|
6ed0fea0e9 |
Task #448: remove gap under Notes header by switching back to sticky
Task #446 made the Notes header `position: fixed` and added an aria-hidden spacer below it sized to the header's measured height (via ResizeObserver). The spacer was exactly the "big empty band" the user kept seeing between the header bar and the "اكتب ملاحظة..." composer — the page background gradient showed through the reserved space. Switched the header back to `sticky top-0 z-20`. Sticky keeps it pinned to the top of the viewport while the page scrolls, but it stays in document flow, so the composer renders immediately under the bar with no artificial gap. Removed the now-unused `headerRef`, `headerHeight` state, ResizeObserver effect, and the spacer div. Bumped composer wrapper padding from `pt-1` back to `pt-3` for a small breathing space (no longer competing with the spacer). No other changes; folders rail, composer behavior, RTL/LTR all untouched. |
||
|
|
4dd28a821c |
Task #448: remove gap under Notes header by switching back to sticky
Task #446 made the Notes header `position: fixed` and added an aria-hidden spacer below it sized to the header's measured height (via ResizeObserver). The spacer was exactly the "big empty band" the user kept seeing between the header bar and the "اكتب ملاحظة..." composer — the page background gradient showed through the reserved space. Switched the header back to `sticky top-0 z-20`. Sticky keeps it pinned to the top of the viewport while the page scrolls, but it stays in document flow, so the composer renders immediately under the bar with no artificial gap. Removed the now-unused `headerRef`, `headerHeight` state, ResizeObserver effect, and the spacer div. Bumped composer wrapper padding from `pt-1` back to `pt-3` for a small breathing space (no longer competing with the spacer). No other changes; folders rail, composer behavior, RTL/LTR all untouched. |
||
|
|
32cda2d86b |
notes: tighten gap above top composer (Task #447)
The composer wrapper that sits directly under the fixed header's height-matching spacer had `pt-4` (16px) on top, which made an obvious empty band between the bottom border of the frozen header and the top of the "اكتب ملاحظة..." card. Reduced that to `pt-1` (4px) so the composer now sits flush under the bar with a minimal breathing space, without affecting the rest of the page (folders rail / browser keep their own `py-6`). artifacts/tx-os/src/pages/notes.tsx:497 — single class change. |
||
|
|
c3999b9af7 |
notes: freeze header + move composer to top of page (Task #446)
User wanted the Notes top bar to truly stay pinned (فريز / ثابت) while scrolling, and wanted the "اكتب ملاحظة..." composer to live at the top of the page above the folders area instead of being tucked under the folders chips and only showing inside a folder. Changes (artifacts/tx-os/src/pages/notes.tsx): - Header bar switched from `sticky top-0` to `fixed top-0 inset-x-0 z-30`. Measured its live height with a `ResizeObserver` + `offsetHeight` (header wraps differently per tab / label set / viewport width), and rendered a same-height aria-hidden spacer immediately below so the first row of content never slides under the bar. - Composer was lifted out of the in-folder branch and is now rendered as the first content block under the spacer (above FoldersRail and FoldersBrowser) on the active "My Notes" view. It is hidden on Inbox, Sent, Archived, and the Shared-with-me read-only folder view, so the read-only contract from Task #445 stays intact. `folderSelection` is still passed in unchanged, so a new note created from this top composer continues to land in the currently-selected folder (or Unfiled if none). - Removed the duplicate Composer mount inside the per-folder branch and left a comment explaining the move. Notes: - No backend or i18n changes — composer placeholder, search input, tabs, label chips and grid/list toggle all kept their existing markup and behavior. - Pre-existing TS errors in artifacts/api-server/src/routes/executive-meetings.ts remain (out of scope) and the `test` workflow continues to fail for that unrelated reason. |
||
|
|
7384711705 |
notes: address review feedback for folder sharing (Task #445)
Re-applied review fixes on top of the live folder-sharing implementation: - PATCH/DELETE /notes/:id now do an id-only lookup and return an explicit 403 when the caller is not the owner (previously returned an ambiguous 404). This makes the read-only contract for shared-folder recipients precise instead of silently masquerading as "not found". - PUT /note-folders/:id/shares rejects self-share with 400 "Cannot share folder with yourself" instead of silently filtering the caller's id out of the recipient set. - GET /note-folders/shared-with-me now includes a `noteCount` (live count of the owner's non-archived notes in the folder) computed via SQL subquery; SharedFolder TS type updated; folders rail renders the count next to each shared folder. - SharedFolderView accepts the page's `search` value and filters the read-only list client-side over title + content + checklist item text, matching the owner's search experience. Empty state distinguishes "no notes" from "no matches". - Added GET /notes?sharedFolderId=:id as a spec-aligned alias that returns the owner's live notes inside a folder shared with the caller (gated by an active row in note_folder_shares). - Updated the stale comment block above the share routes that claimed recipient writes "just 404"; documents the new 403 contract. Deviations / out of scope: - Project uses `drizzle-kit push` (no migrations directory); no SQL migration file was added. - Pre-existing TS errors in artifacts/api-server/src/routes/executive-meetings.ts remain; unrelated to Task #445. - "Strict reject for nonexistent recipient ids in share PUT" left as optional follow-up (currently silently dropped per existing behavior). |
||
|
|
a4949983f3 |
Notes: live folder-sharing (read-only) — Task #445
Owner can share a whole folder with other users; recipients see it
under "Shared with me" in the folders rail and view notes read-only
(cannot edit, add, move, or delete).
- DB: new noteFolderSharesTable (folderId/recipientUserId, cascade,
unique idx). Pushed via drizzle-kit.
- API (artifacts/api-server/src/routes/notes.ts):
- GET/PATCH/POST /note-folders return sharedWithCount.
- GET /note-folders/shared-with-me, GET /note-folders/:id/shares,
PUT /note-folders/:id/shares (idempotent diff), DELETE
/note-folders/:id/shares/:userId, GET /note-folders/:id/shared-notes
(verifies recipient via noteFolderSharesTable, stamps readOnly).
- Emits note-folder-shared / note-folder-unshared on share changes.
- Existing note write paths remain owner-scoped → recipients can't
mutate owner notes.
- Frontend types/hooks (notes-api.ts): SharedFolder, FolderShareRecipient,
SharedFolderNote/View; useFolderShares, useUpdateFolderShares,
useSharedWithMeFolders, useSharedFolderNotes.
- FoldersRail: Share menu item, sharedWithCount badge, "Shared with me"
section, "shared-folder" selection kind, onShareFolder prop.
- notes.tsx: SharedFolderView (no Composer, read-only cards using
ChecklistView), FolderShareDialog (seeds existing recipients,
idempotent PUT on save), revocation fallback effect, dialog mount.
- use-notifications-socket.ts: subscribe to note-folder-shared /
-unshared → invalidate shared-with-me + open shared-folder notes for
live rail/view refresh.
- i18n: AR/EN keys for share/sharedBy/sharedByName/sharedWithCount/
sharedWithMe/readOnly/shareRevoked/sharedEmpty/shareSaved/shareFailed.
Pre-existing failing `test` workflow (executive-meetings type errors)
is unrelated and out of scope.
|
||
|
|
5ef0250195 |
Task #444: Add Send button to note composer
Problem: the new-note composer in the Notes page only exposed Save — to send a freshly-typed note the user had to save it, hover the resulting card, and click the small Send icon. The user asked for an explicit Send action right inside the composer and confirmed they're fine with auto-saving as a side effect of pressing Send. Implementation (artifacts/tx-os/src/pages/notes.tsx): - Added `onSend: (id: number) => void` prop to <Composer> and wired the NotesPage call site to forward to the existing `setSendForNoteId(id)` flow that already mounts <SendNoteDialog>. - Extracted a `buildPayload()` helper from save() so save() and the new sendNew() use the exact same payload shape (title, content, color, labels, kind, items, folderId). - Added `sendNew()` that runs the same `create.mutate` save and, on success, resets the composer and calls `onSend(created.id)` so the send dialog opens for the freshly created note. On failure the composer state is left intact (same UX as save() failures). - Added a Send icon button next to Save inside the composer footer, aria-labeled with the existing `notes.send` translation and tagged `data-testid="notes-composer-send"`. The button is disabled while the composer is empty (no title and no content/checklist text) and while a create request is in flight to prevent duplicate notes from rapid clicks. The button lives inside the composer's `ref` so it doesn't trip the existing click-outside-to-save handler, and <SendNoteDialog> renders in a Radix portal which is already excluded from that handler. No changes to send dialog logic, the per-card hover Send button, or any other surface. tsc --noEmit passes for tx-os. The pre-existing api-server `test` workflow failures (executive-meetings, service- orders) are unrelated and out of scope. |
||
|
|
2a89b38b04 |
Task #443: Fix notes search icon overlapping placeholder in Arabic
Problem: in the Notes page header search field, the magnifier icon is
absolutely positioned on the start side (right in RTL, left in LTR)
via inline style, but the input's reserved padding was using
`isRtl ? "pe-9" : "ps-9"`. In RTL, `pe-9` resolves to padding-LEFT —
the opposite side from the icon — so the placeholder text "ابحث في
الملاحظات" started flush to the right edge and visibly overlapped
the icon. In LTR it happened to look correct because both `ps-9` and
the icon's `left: 10` line up.
Fix: pin the padding to the start side unconditionally
(`className="ps-9"`) so it always matches the icon's start-side
placement in both directions. Added an inline comment documenting
the invariant ("icon on start side ⇒ padding on start side") so a
future edit doesn't re-introduce the asymmetry.
Files:
- artifacts/tx-os/src/pages/notes.tsx (search input around line 304-321)
No behavioural changes to LTR rendering, no other search inputs
affected (only this one bound the padding to `isRtl`). Verified with
`pnpm exec tsc --noEmit` (clean). The pre-existing failing `test`
workflow stems from unrelated api-server tsc errors in
executive-meetings.ts and is out of scope for this task.
|
||
|
|
8d207881ef |
Update default note color to a neutral accent for better contrast
Change the default note's accent color to slate-500 for improved visibility and consistency in `artifacts/tx-os/src/lib/notes-api.ts`. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: fccf6ea1-57dc-40bb-8469-1dafe358e639 Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/W25NwW4 Replit-Helium-Checkpoint-Created: true |
||
|
|
11439abd39 |
Task #442: Checklist checkbox color matches the note's color
- artifacts/tx-os/src/lib/notes-api.ts: extended NOTE_COLORS so each entry carries an `accent` Tailwind class alongside `bg`/`ring` (default→amber, red→red, orange→orange, yellow→yellow, green→green, teal→teal, blue→sky, purple→purple, pink→pink, gray→slate; all *-500 for visibility in light + dark). Added a `colorAccent(id)` helper that resolves to a literal accent-* class, falling back to the default amber for unknown/null ids. The class strings are written out as literals so Tailwind v4's source scanner emits all of them into the generated CSS — no dynamic class names, no safelist needed. - artifacts/tx-os/src/pages/notes.tsx: ChecklistView gained an optional `noteColor?: string | null` prop and now resolves its checkbox accent via `colorAccent(noteColor)` instead of the hard-coded `accent-amber-500`. Updated all four call sites (NoteCard, received list, sent list, thread dialog) to pass `note.color` / `n.color` / `thread.color`. - artifacts/tx-os/src/components/notes/incoming-note-popup.tsx: PopupChecklistPreview gained the same optional `noteColor` prop and uses `colorAccent` for the checkbox className. Call site now passes `current.color` from the popup queue. - Unchecked boxes still render with the neutral browser default; only the checked accent (the filled tint) follows the note color. The default (white/card) note keeps amber for visibility against the neutral surface in both themes. - Verified: `pnpm exec tsc --noEmit` is clean and the `tests/notes-popup-on-receive.spec.mjs` regression suite (incl. the Task #433 checklist popup case) still passes 3/3. - Pre-existing failing `test` workflow (api-server tsc errors in executive-meetings.ts) is unrelated and out of scope. |
||
|
|
28ddbe33d1 |
Task #441: Fix notification sound on iPad/Safari (Web Audio migration)
- Rewrote artifacts/tx-os/src/lib/notification-sounds.ts to use the
Web Audio API (lazy AudioContext + decoded AudioBuffer cache) instead
of HTMLAudioElement. unlock() resumes the ctx for any non-running
state (handles iOS `interrupted`), plays a 1-sample silent buffer to
satisfy iOS Safari's gesture requirement, and pre-warms decodes.
decodeAudioData wrapped to support both Promise and callback forms
for older WebKit.
- play() dispatches an AUTOPLAY_BLOCKED_EVENT with `{ isIos }` detail
when ctx isn't running. Throttle is 3 s; new bypassThrottle option
(used by testPlay) skips the gate AND avoids advancing lastPlayAt
so settings previews can't suppress real socket chimes.
- use-audio-unlock.ts keeps gesture listeners attached and re-calls
unlock() on every gesture (handles iPad ctx auto-suspend on
background / AirPlay).
- use-autoplay-hint.ts reads detail.isIos and switches between
notifSettings.autoplayHint and notifSettings.autoplayHintIos
(added to en.json + ar.json with Control Center / silent-switch
guidance).
- notification-settings.tsx per-sound preview button now calls
notificationPlayer.testPlay(id).
- Test globals __txosNotifPlayCount/__txosNotifLastSound preserved;
added __txosNotifCtxState() accessor (dev/test-only, gated by
import.meta.env.MODE).
- Added tests/notification-sound-webaudio.spec.mjs covering the
preview button gesture path: counter increments, throttle bypass
works, singleton AudioContext reaches `running` state.
Notes:
- Pre-existing api-server tsc errors in executive-meetings.ts and the
failing `test` workflow are unrelated to this task and out of scope.
|
||
|
|
2ac659bf23 |
Improve drag performance for meeting alerts on touch devices
Apply `touch-action: none` to the drag handle and `pointer-events: none` to the ping animation overlay in `upcoming-meeting-alert.tsx` to fix drag stuttering on iPad. Replit-Commit-Author: Agent Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66 Replit-Commit-Checkpoint-Type: full_checkpoint Replit-Commit-Event-Id: 4114b48d-7679-479b-a57c-9c2f1ab9674b Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/W25NwW4 Replit-Helium-Checkpoint-Created: true |
||
|
|
ee0344a9d3 |
Task #438: collaborative checklists + meeting alert animation parity
Backend - New POST /notes/:id/checklist/:itemId/toggle endpoint. Owner or any active (non-archived) recipient can flip an item's done flag. - Wrapped in a DB transaction with SELECT ... FOR UPDATE on the live note row so concurrent toggles from multiple collaborators can't lose each other's updates. Live notes.items + every note_recipients.items snapshot are mirrored atomically in the same tx. - Emits note_checklist_changed to the audience minus the actor with the full updated items array so receivers can patch state without an extra fetch. Frontend - useToggleChecklistItem mutation hook with optimistic updates across notes/sent/received/thread caches and rollback on error. - Incoming-note popup checklist is now interactive with a local override for instant visual feedback (popup renders from socket payload, not from the query cache). - Thread checklist toggles are gated by effective permission (owner, admin, or non-archived recipient) to mirror server auth. - Socket handler for note_checklist_changed invalidates relevant query keys AND patches the open popup payload directly via a new IncomingNotePopupContext.updateChecklistItems action so collaborators' open popups stay in sync. Meeting alert animation parity (upcoming-meeting-alert.tsx) - Added scale-95 -> scale-100 + fade entrance with the same spring cubic-bezier the note popup uses, retriggered per eligible meeting. - Replaced the 1px border with a ring-4 colored frame driven by alertPrefs.accent (via boxShadow so the color is dynamic). - Added an animate-ping accent halo around the drag-handle icon to match the popup's avatar pulse. Tests - artifacts/api-server/tests/notes-checklist-toggle.test.mjs (7 tests: owner/recipient toggle + mirroring, 403 non-recipient, 403 archived, 400 non-checklist, 404 unknown item, 400 invalid body). - artifacts/tx-os/tests/notes-popup-checklist-collab.spec.mjs (e2e: recipient ticks an item from the popup, server + sender snapshot both reflect the change). Code review (architect) findings addressed: - (critical) lost-update race -> tx with row lock. - (critical) non-atomic snapshot mirror -> same tx. - (critical) open popup didn't re-render on socket fanout -> updateChecklistItems context action. - UI permission parity for archived recipients -> thread gates onToggle. |
||
|
|
093f42665c |
Task #438: collaborative checklists + meeting alert animation parity
Backend - New POST /notes/:id/checklist/:itemId/toggle endpoint. Owner or any active (non-archived) recipient can flip an item's done flag. - Wrapped in a DB transaction with SELECT ... FOR UPDATE on the live note row so concurrent toggles from multiple collaborators can't lose each other's updates. Live notes.items + every note_recipients.items snapshot are mirrored atomically in the same tx. - Emits note_checklist_changed to the audience minus the actor with the full updated items array so receivers can patch state without an extra fetch. Frontend - useToggleChecklistItem mutation hook with optimistic updates across notes/sent/received/thread caches and rollback on error. - Incoming-note popup checklist is now interactive with a local override for instant visual feedback (popup renders from socket payload, not from the query cache). - Thread checklist toggles are gated by effective permission (owner, admin, or non-archived recipient) to mirror server auth. - Socket handler for note_checklist_changed invalidates relevant query keys AND patches the open popup payload directly via a new IncomingNotePopupContext.updateChecklistItems action so collaborators' open popups stay in sync. Meeting alert animation parity (upcoming-meeting-alert.tsx) - Added scale-95 -> scale-100 + fade entrance with the same spring cubic-bezier the note popup uses, retriggered per eligible meeting. - Replaced the 1px border with a ring-4 colored frame driven by alertPrefs.accent (via boxShadow so the color is dynamic). - Added an animate-ping accent halo around the drag-handle icon to match the popup's avatar pulse. Tests - artifacts/api-server/tests/notes-checklist-toggle.test.mjs (7 tests: owner/recipient toggle + mirroring, 403 non-recipient, 403 archived, 400 non-checklist, 404 unknown item, 400 invalid body). - artifacts/tx-os/tests/notes-popup-checklist-collab.spec.mjs (e2e: recipient ticks an item from the popup, server + sender snapshot both reflect the change). Code review (architect) findings addressed: - (critical) lost-update race -> tx with row lock. - (critical) non-atomic snapshot mirror -> same tx. - (critical) open popup didn't re-render on socket fanout -> updateChecklistItems context action. - UI permission parity for archived recipients -> thread gates onToggle. |
||
|
|
10c1e641b4 |
Task #438: collaborative checklists + meeting alert animation parity
Backend - New POST /notes/:id/checklist/:itemId/toggle endpoint. Owner or any active (non-archived) recipient can flip an item's done flag. - Wrapped in a DB transaction with SELECT ... FOR UPDATE on the live note row so concurrent toggles from multiple collaborators can't lose each other's updates. Live notes.items + every note_recipients.items snapshot are mirrored atomically in the same tx. - Emits note_checklist_changed to the audience minus the actor with the full updated items array so receivers can patch state without an extra fetch. Frontend - useToggleChecklistItem mutation hook with optimistic updates across notes/sent/received/thread caches and rollback on error. - Incoming-note popup checklist is now interactive with a local override for instant visual feedback (popup renders from socket payload, not from the query cache). - Thread checklist toggles are gated by effective permission (owner, admin, or non-archived recipient) to mirror server auth. - Socket handler for note_checklist_changed invalidates relevant query keys AND patches the open popup payload directly via a new IncomingNotePopupContext.updateChecklistItems action so collaborators' open popups stay in sync. Meeting alert animation parity (upcoming-meeting-alert.tsx) - Added scale-95 -> scale-100 + fade entrance with the same spring cubic-bezier the note popup uses, retriggered per eligible meeting. - Replaced the 1px border with a ring-4 colored frame driven by alertPrefs.accent (via boxShadow so the color is dynamic). - Added an animate-ping accent halo around the drag-handle icon to match the popup's avatar pulse. Tests - artifacts/api-server/tests/notes-checklist-toggle.test.mjs (7 tests: owner/recipient toggle + mirroring, 403 non-recipient, 403 archived, 400 non-checklist, 404 unknown item, 400 invalid body). - artifacts/tx-os/tests/notes-popup-checklist-collab.spec.mjs (e2e: recipient ticks an item from the popup, server + sender snapshot both reflect the change). Code review (architect) findings addressed: - (critical) lost-update race -> tx with row lock. - (critical) non-atomic snapshot mirror -> same tx. - (critical) open popup didn't re-render on socket fanout -> updateChecklistItems context action. - UI permission parity for archived recipients -> thread gates onToggle. |
||
|
|
3d8840afee |
Task #438: collaborative checklists + meeting alert animation parity
Backend - New POST /notes/:id/checklist/:itemId/toggle endpoint. Owner or any active (non-archived) recipient can flip an item's done flag. - Wrapped in a DB transaction with SELECT ... FOR UPDATE on the live note row so concurrent toggles from multiple collaborators can't lose each other's updates. Live notes.items + every note_recipients.items snapshot are mirrored atomically in the same tx. - Emits note_checklist_changed to the audience minus the actor with the full updated items array so receivers can patch state without an extra fetch. Frontend - useToggleChecklistItem mutation hook with optimistic updates across notes/sent/received/thread caches and rollback on error. - Incoming-note popup checklist is now interactive with a local override for instant visual feedback (popup renders from socket payload, not from the query cache). - Thread checklist toggles are gated by effective permission (owner, admin, or non-archived recipient) to mirror server auth. - Socket handler for note_checklist_changed invalidates relevant query keys AND patches the open popup payload directly via a new IncomingNotePopupContext.updateChecklistItems action so collaborators' open popups stay in sync. Meeting alert animation parity (upcoming-meeting-alert.tsx) - Added scale-95 -> scale-100 + fade entrance with the same spring cubic-bezier the note popup uses, retriggered per eligible meeting. - Replaced the 1px border with a ring-4 colored frame driven by alertPrefs.accent (via boxShadow so the color is dynamic). - Added an animate-ping accent halo around the drag-handle icon to match the popup's avatar pulse. Tests - artifacts/api-server/tests/notes-checklist-toggle.test.mjs (7 tests: owner/recipient toggle + mirroring, 403 non-recipient, 403 archived, 400 non-checklist, 404 unknown item, 400 invalid body). - artifacts/tx-os/tests/notes-popup-checklist-collab.spec.mjs (e2e: recipient ticks an item from the popup, server + sender snapshot both reflect the change). Code review (architect) findings addressed: - (critical) lost-update race -> tx with row lock. - (critical) non-atomic snapshot mirror -> same tx. - (critical) open popup didn't re-render on socket fanout -> updateChecklistItems context action. - UI permission parity for archived recipients -> thread gates onToggle. |
||
|
|
6292d52ffa |
Task #438: collaborative checklists + meeting alert animation parity
Backend - New POST /notes/:id/checklist/:itemId/toggle endpoint. Owner or any active (non-archived) recipient can flip an item's done flag. - Wrapped in a DB transaction with SELECT ... FOR UPDATE on the live note row so concurrent toggles from multiple collaborators can't lose each other's updates. Live notes.items + every note_recipients.items snapshot are mirrored atomically in the same tx. - Emits note_checklist_changed to the audience minus the actor with the full updated items array so receivers can patch state without an extra fetch. Frontend - useToggleChecklistItem mutation hook with optimistic updates across notes/sent/received/thread caches and rollback on error. - Incoming-note popup checklist is now interactive with a local override for instant visual feedback (popup renders from socket payload, not from the query cache). - Thread checklist toggles are gated by effective permission (owner, admin, or non-archived recipient) to mirror server auth. - Socket handler for note_checklist_changed invalidates relevant query keys AND patches the open popup payload directly via a new IncomingNotePopupContext.updateChecklistItems action so collaborators' open popups stay in sync. Meeting alert animation parity (upcoming-meeting-alert.tsx) - Added scale-95 -> scale-100 + fade entrance with the same spring cubic-bezier the note popup uses, retriggered per eligible meeting. - Replaced the 1px border with a ring-4 colored frame driven by alertPrefs.accent (via boxShadow so the color is dynamic). - Added an animate-ping accent halo around the drag-handle icon to match the popup's avatar pulse. Tests - artifacts/api-server/tests/notes-checklist-toggle.test.mjs (7 tests: owner/recipient toggle + mirroring, 403 non-recipient, 403 archived, 400 non-checklist, 404 unknown item, 400 invalid body). - artifacts/tx-os/tests/notes-popup-checklist-collab.spec.mjs (e2e: recipient ticks an item from the popup, server + sender snapshot both reflect the change). Code review (architect) findings addressed: - (critical) lost-update race -> tx with row lock. - (critical) non-atomic snapshot mirror -> same tx. - (critical) open popup didn't re-render on socket fanout -> updateChecklistItems context action. - UI permission parity for archived recipients -> thread gates onToggle. |