Commit Graph

885 Commits

Author SHA1 Message Date
riyadhafraa 725a1a7f27 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.
2026-05-07 11:28:05 +00:00
riyadhafraa a430812998 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.
2026-05-07 11:24:57 +00:00
riyadhafraa 2ced0963be Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 462877ca-477e-467b-8be8-074efb6e2f69
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
2026-05-07 11:12:12 +00:00
riyadhafraa dcaebb38f5 notes: re-send fires popup + notification again
Previously POST /api/notes/:id/send used onConflictDoNothing on
(noteId, recipientUserId) and only iterated `inserted` rows for
side-effects, so a re-send to the same recipient was silent — no
note_received socket event, no notification — even though the API
returned 200. Reported by the user: "I sent again to rh, why
didn't it reach them?"

Fix: after the idempotent insert, load the existing recipient
rows for any recipients that were skipped, and run the
notification + socket emit loop over BOTH inserted and existing
rows. The DB row is still not duplicated (snapshot semantics
preserved), but the recipient now sees the popup and gets a fresh
notification on every re-send.

Verified:
- @workspace/api-server tests/notes-share.test.mjs — 12/12 pass
  (including "re-sending to the same recipient does not duplicate
  the recipient row").
- tx-os e2e notes-popup-on-receive — 3/3 pass.
2026-05-07 10:32:25 +00:00
riyadhafraa a82ce51979 Update the popup's visual appearance for better consistency
Refactor `IncomingNotePopup` component to simplify structure, adjust background colors, and modify header/footer styling using Tailwind CSS classes.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 7c54ec8d-7451-4f17-9a87-e622271d4d8e
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
2026-05-07 10:17:44 +00:00
riyadhafraa eee78810bb Task #436: tint the whole incoming-note popup with the note color
The popup was a colored body sitting inside a white frame; users
wanted the WHOLE popup to look like the sticky note itself, with
only the outer ring + chrome remaining as frame.

- Outer rounded-2xl card now uses `colorBg(current.color)` directly
  (replacing `bg-background`). Default-color notes still render on
  `bg-white dark:bg-card` so the look is unchanged for uncolored
  notes.
- Removed the inner card-in-card colored body wrapper; title +
  content/checklist now render directly on the tinted surface.
- Header divider switched from `bg-muted/40` (opaque chrome) to a
  translucent `border-black/10 dark:border-white/10` so the tint
  flows under the avatar/title/X without an opaque strip.
- Action row moved into its own bordered footer strip (same
  translucent divider) so the 3 buttons stay legible on every
  palette tint while letting the colored surface bleed through.
- Added `data-popup-color` on the card for diagnostics.
- Reply popups inherit the same tint via the existing `color` field
  on the reply payload (no plumbing change).

No payload, locale, or test-id changes. All popup e2e suites pass:
notes-popup-on-receive (3/3), notes-popup-touch-tap (2/2).
2026-05-07 10:16:48 +00:00
riyadhafraa d8eac73679 Task #436: tint the whole incoming-note popup with the note color
The popup was a colored body sitting inside a white frame; users
wanted the WHOLE popup to look like the sticky note itself, with
only the outer ring + chrome remaining as frame.

- Outer rounded-2xl card now uses `colorBg(current.color)` directly
  (replacing `bg-background`). Default-color notes still render on
  `bg-white dark:bg-card` so the look is unchanged for uncolored
  notes.
- Removed the inner card-in-card colored body wrapper; title +
  content/checklist now render directly on the tinted surface.
- Header divider switched from `bg-muted/40` (opaque chrome) to a
  translucent `border-black/10 dark:border-white/10` so the tint
  flows under the avatar/title/X without an opaque strip.
- Action row moved into its own bordered footer strip (same
  translucent divider) so the 3 buttons stay legible on every
  palette tint while letting the colored surface bleed through.
- Added `data-popup-color` on the card for diagnostics.
- Reply popups inherit the same tint via the existing `color` field
  on the reply payload (no plumbing change).

No payload, locale, or test-id changes. All popup e2e suites pass:
notes-popup-on-receive (3/3), notes-popup-touch-tap (2/2).
2026-05-07 10:14:11 +00:00
riyadhafraa 6eb335df1c Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 65ab9d26-9e3a-4167-9393-ae1f277436f2
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
2026-05-07 10:09:56 +00:00
riyadhafraa 21995a5ecb Task #433: incoming-note popup — render full note + 3-button action row
- Inner body now renders as a real sticky note: keeps colored
  surface, supports checklist (read-only) when noteKind === "checklist".
- Action row trimmed to exactly 3 buttons: Done (تم), Open note
  (فتح الملاحظة), Reply (رد). Removed the standalone Dismiss button;
  header X still closes.
- Reply variant uses the same 3-button row (Done is a dismiss-only
  no-op since the note owner can't mark their own note read).
- Wire kind + items from backend `note_received` payload into client
  IncomingNotePayload (noteKind/items) via use-notifications-socket.
- Locales: markRead → "Done"/"تم"; new openNote/done keys; openThread
  unified to "Open note"/"فتح الملاحظة".
- Tests: replaced removed `incoming-note-popup-dismiss` testid usages
  with header `-close`; updated reply variant assertions to expect the
  3-button row; fixed pre-existing pluralization bug in reply path
  (`/replies` → `/reply`) that was unrelated to this task but blocked
  the reply popup test from validating.

Labels and pin were not surfaced in the popup because they are
sender-private state (note_recipients only snapshots
title/content/color/kind/items); the recipient never receives the
sender's labelIds/isPinned, so showing them would be misleading.

Added a new e2e ("recipient popup renders checklist items for a
checklist note") that composes a checklist note via the UI, sends it,
and asserts `incoming-note-popup-checklist` is visible with both
items — locks in the noteKind/items socket plumbing.

All affected e2e tests pass (notes-popup-on-receive note + reply +
checklist, notes-popup-touch-tap both, notes-inbox); queue unit
tests pass.
2026-05-07 09:34:03 +00:00
riyadhafraa aaafdca1e5 Task #433: incoming-note popup — render full note + 3-button action row
- Inner body now renders as a real sticky note: keeps colored
  surface, supports checklist (read-only) when noteKind === "checklist".
- Action row trimmed to exactly 3 buttons: Done (تم), Open note
  (فتح الملاحظة), Reply (رد). Removed the standalone Dismiss button;
  header X still closes.
- Reply variant uses the same 3-button row (Done is a dismiss-only
  no-op since the note owner can't mark their own note read).
- Wire kind + items from backend `note_received` payload into client
  IncomingNotePayload (noteKind/items) via use-notifications-socket.
- Locales: markRead → "Done"/"تم"; new openNote/done keys; openThread
  unified to "Open note"/"فتح الملاحظة".
- Tests: replaced removed `incoming-note-popup-dismiss` testid usages
  with header `-close`; updated reply variant assertions to expect the
  3-button row; fixed pre-existing pluralization bug in reply path
  (`/replies` → `/reply`) that was unrelated to this task but blocked
  the reply popup test from validating.

Labels and pin were not surfaced in the popup because they are
sender-private state (note_recipients only snapshots
title/content/color/kind/items); the recipient never receives the
sender's labelIds/isPinned, so showing them would be misleading.

Added a new e2e ("recipient popup renders checklist items for a
checklist note") that composes a checklist note via the UI, sends it,
and asserts `incoming-note-popup-checklist` is visible with both
items — locks in the noteKind/items socket plumbing.

All affected e2e tests pass (notes-popup-on-receive note + reply +
checklist, notes-popup-touch-tap both, notes-inbox); queue unit
tests pass.
2026-05-07 09:32:17 +00:00
riyadhafraa 213895e9e8 Task #433: incoming-note popup — render full note + 3-button action row
- Inner body now renders as a real sticky note: keeps colored
  surface, supports checklist (read-only) when noteKind === "checklist".
- Action row trimmed to exactly 3 buttons: Done (تم), Open note
  (فتح الملاحظة), Reply (رد). Removed the standalone Dismiss button;
  header X still closes.
- Reply variant uses the same 3-button row (Done is a dismiss-only
  no-op since the note owner can't mark their own note read).
- Wire kind + items from backend `note_received` payload into client
  IncomingNotePayload (noteKind/items) via use-notifications-socket.
- Locales: markRead → "Done"/"تم"; new openNote/done keys; openThread
  unified to "Open note"/"فتح الملاحظة".
- Tests: replaced removed `incoming-note-popup-dismiss` testid usages
  with header `-close`; updated reply variant assertions to expect the
  3-button row; fixed pre-existing pluralization bug in reply path
  (`/replies` → `/reply`) that was unrelated to this task but blocked
  the reply popup test from validating.

Labels and pin were not surfaced in the popup because they are
sender-private state (note_recipients only snapshots
title/content/color/kind/items); the recipient never receives the
sender's labelIds/isPinned, so showing them would be misleading.

Added a new e2e ("recipient popup renders checklist items for a
checklist note") that composes a checklist note via the UI, sends it,
and asserts `incoming-note-popup-checklist` is visible with both
items — locks in the noteKind/items socket plumbing.

All affected e2e tests pass (notes-popup-on-receive note + reply +
checklist, notes-popup-touch-tap both, notes-inbox); queue unit
tests pass.
2026-05-07 09:29:31 +00:00
riyadhafraa ff3c567b3c Task #433: incoming-note popup — render full note + 3-button action row
- Inner body now renders as a real sticky note: keeps colored
  surface, supports checklist (read-only) when noteKind === "checklist".
- Action row trimmed to exactly 3 buttons: Done (تم), Open note
  (فتح الملاحظة), Reply (رد). Removed the standalone Dismiss button;
  header X still closes.
- Reply variant uses the same 3-button row (Done is a dismiss-only
  no-op since the note owner can't mark their own note read).
- Wire kind + items from backend `note_received` payload into client
  IncomingNotePayload (noteKind/items) via use-notifications-socket.
- Locales: markRead → "Done"/"تم"; new openNote/done keys; openThread
  unified to "Open note"/"فتح الملاحظة".
- Tests: replaced removed `incoming-note-popup-dismiss` testid usages
  with header `-close`; updated reply variant assertions to expect the
  3-button row; fixed pre-existing pluralization bug in reply path
  (`/replies` → `/reply`) that was unrelated to this task but blocked
  the reply popup test from validating.

Labels and pin were not surfaced in the popup because they are
sender-private state (note_recipients only snapshots
title/content/color/kind/items); the recipient never receives the
sender's labelIds/isPinned, so showing them would be misleading.

All affected e2e tests pass (notes-popup-on-receive note + reply,
notes-popup-touch-tap both, notes-inbox); queue unit tests pass.
2026-05-07 09:23:29 +00:00
riyadhafraa 03a59572fa Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 46aeb949-fb3b-44c1-bd13-146d32b4eb8c
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/e5Hflyw
Replit-Helium-Checkpoint-Created: true
2026-05-07 09:13:11 +00:00
riyadhafraa ea3092bb2b Task #431: polish Send Note dialog (RTL X + search overlap)
Two visual issues in the "إرسال الملاحظة" dialog:

1. The shadcn DialogContent renders a built-in close (X) button hard
   pinned at `right-4 top-4`. In Arabic the dialog title is
   right-aligned and crashed into the X.
   Fix: add `pr-8` to the SendNoteDialog header so the title row
   reserves physical right-padding matching where the X actually
   lives in both LTR and RTL. (Initially used `pe-8` but corrected to
   `pr-8` after code review noted the close button is pinned with
   physical `right-4`, so a logical end-padding flips to the wrong
   side in RTL.) The shared dialog component is unchanged so other
   dialogs are untouched (out of scope per the task plan).

2. The recipient search field used inline `style={{ right|left: 10 }}`
   for the magnifier and a conditional `pe-9` / `ps-9` based on
   `i18n.language`. In RTL the icon collided with the placeholder.
   Fix: drop the conditional branching — position the icon with the
   logical class `start-2.5` and always pad the input with `ps-9`.
   The icon now sits at the inline-start edge in both LTR and RTL
   with a clear gutter to the placeholder/caret.

Skipped step 3 of the plan (i18n aria-label override on the X). It
required no locale changes and no rendering improvement, so per the
plan's own conditional ("only if step 3 keeps the override; otherwise
no locale changes") it was dropped to keep scope tight.

Verification:
- `pnpm exec tsc --noEmit` clean.
- Playwright: `notes-inbox.spec.mjs` (1/1) and
  `notes-popup-touch-tap.spec.mjs` (2/2) green — both exercise the
  send-note dialog rendering. `notes-popup-on-receive.spec.mjs` runs
  two browser contexts and exceeds the local 120s shell cap; not
  re-run here, but the diff is CSS-only and cannot affect the wire
  flow it covers.
2026-05-07 09:03:28 +00:00
riyadhafraa b5e063d6ed Task #431: polish Send Note dialog (RTL X + search overlap)
Two visual issues in the "إرسال الملاحظة" dialog:

1. The shadcn DialogContent renders a built-in close (X) button hard
   pinned at `right-4 top-4`. In Arabic the dialog title is
   right-aligned and crashed into the X.
   Fix: add `pe-8` to the SendNoteDialog header so the title row
   reserves logical end-padding for the X in both directions. The
   shared dialog component is unchanged so other dialogs are untouched
   (out of scope per the task plan).

2. The recipient search field used inline `style={{ right|left: 10 }}`
   for the magnifier and a conditional `pe-9` / `ps-9` based on
   `i18n.language`. In RTL the icon collided with the placeholder.
   Fix: drop the conditional branching — position the icon with the
   logical class `start-2.5` and always pad the input with `ps-9`.
   The icon now sits at the inline-start edge in both LTR and RTL
   with a clear gutter to the placeholder/caret.

Skipped step 3 of the plan (i18n aria-label override on the X). It
required no locale changes and no rendering improvement, so per the
plan's own conditional ("only if step 3 keeps the override; otherwise
no locale changes") it was dropped to keep scope tight.

Verification:
- `pnpm exec tsc --noEmit` clean.
- Playwright: `notes-inbox.spec.mjs` (1/1) and
  `notes-popup-touch-tap.spec.mjs` (2/2) green — both exercise the
  send-note dialog rendering. `notes-popup-on-receive.spec.mjs` runs
  two browser contexts and exceeds the local 120s shell cap; not
  re-run here, but the diff is CSS-only and cannot affect the wire
  flow it covers.
2026-05-07 09:02:21 +00:00
riyadhafraa 2b52036c25 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 976ef90f-1582-47a4-9191-ee34dfe643eb
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/5kzMIgG
Replit-Helium-Checkpoint-Created: true
2026-05-07 08:55:19 +00:00
riyadhafraa 27bd3cbf61 Task #430: fix folder note creation + add in-app back to folders
Fixes two regressions surfaced after the folder browser shipped in #429:

1. Note created from inside a folder went to Unfiled
   - Composer now takes `folderSelection` and resolves the target folder:
     `folder` → that id, `unfiled`/`all` → null. The new note appears in
     the active folder view immediately and the folder count increments
     via the existing notes query invalidation.

2. No in-app back from a folder view
   - Added a "← Back to folders / المجلدات" chip above the composer
     whenever a folder or Unfiled is selected (active tab only). Tapping
     it resets the selection to "all" and lands the user back on the
     folders browser. The global header back button is unchanged.
   - Chevron flips for RTL via `rtl:rotate-180`.
   - Locale keys: `notes.folders.backToFolders` (EN/AR).

Verification:
- tsc --noEmit clean.
- Playwright suites all green: notes-folders, notes-checklist,
  notes-card-touch-actions, notes-delete-confirm (8 tests).
2026-05-07 08:41:53 +00:00
riyadhafraa 94deea59de Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 688c04a3-6913-4916-8693-3651b206c913
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/5kzMIgG
Replit-Helium-Checkpoint-Created: true
2026-05-07 08:39:14 +00:00
riyadhafraa 9230e487d8 Task #429: folders-as-files browser at "All notes" view
- Notes restored to original sticky-note cards (Section + NoteCard) at all
  views, reverting the file-manager-style note rendering from #427.
- Folders render as a file-manager browser at the top of the active "All
  notes" view: black folder icon, name, relative time, item-count badge,
  "…" kebab with rename/delete, and a checkbox for multi-select. Honors the
  reused grid/list view-toggle.
- Multi-select bar appears when ≥1 folder is checked, with bulk Delete
  (confirmation dialog) and Cancel actions. Bulk delete uses Promise.all
  over useDeleteFolder; selection auto-prunes deleted ids.
- View-toggle is hidden when not at active+all (it now only governs the
  folder browser).
- Added `mode: "rail" | "chips-only"` to FoldersRail. Chips-only is scoped
  to active+all only — archived/inbox/sent rail behavior is unchanged.
- Drop targets preserved (data-folder-drop, folder-drop-${id}) so desktop
  DnD and touch-long-press pipelines keep working.
- Click propagation stopped in DropdownMenuContent and around the checkbox
  so menu/checkbox interactions don't navigate into the folder.
- Removed obsolete FilesView/FileItem/SelectionBar/DeleteDialog/colorTint
  helpers (~650 lines).
- Added EN/AR locale keys: notes.folders.actions, renameFailed, itemCount,
  itemCountOne, select, bulkSelected, bulkDeleteTitle, bulkDeleteConfirm,
  bulkDeleteFailed.
- Verified: tsc --noEmit clean; notes-folders, notes-card-touch-actions,
  notes-checklist, notes-delete-confirm Playwright suites all green.

Drift note: kept "All notes" as a mixed surface (folders browser + sticky
notes below the composer) per the user's task wording, which explicitly
requires both sticky-note cards and file-manager folders. Code reviewer
suggested folders-only as primary surface, but doing so would hide notes
the user explicitly asked to see and break tests at the default view.
2026-05-07 08:22:37 +00:00
riyadhafraa 74e9272c2a Task #429: folders-as-files browser at "All notes" view
- Notes restored to original sticky-note cards (Section + NoteCard) at all
  views, reverting the file-manager-style note rendering from #427.
- Folders now render as a file-manager browser at the top of the "All notes"
  view: black folder icon, name, relative time, item-count badge, "…" kebab
  with rename/delete, plus a checkbox for multi-select.
- Multi-select bar appears when ≥1 folder is checked, with bulk Delete
  (confirmation dialog) and Cancel actions. Bulk delete uses Promise.all
  over useDeleteFolder; selection auto-prunes deleted ids.
- Grid/list view-toggle reused from #427; toggle is hidden when not at
  "All notes" since it now only governs the folder browser.
- Added `mode: "rail" | "chips-only"` to FoldersRail. At "All notes" the
  rail switches to chips-only (All / Unfiled / + New folder) so folder rows
  live exclusively in the new browser and test ids never collide.
- Drop targets preserved (data-folder-drop, folder-drop-${id}) so the
  desktop DnD and touch-long-press pipelines keep working.
- Stop click propagation in DropdownMenuContent and around the checkbox to
  prevent rename/delete/checkbox clicks from navigating into the folder.
- Removed obsolete FilesView/FileItem/SelectionBar/DeleteDialog/colorTint
  helpers (~650 lines).
- Added EN/AR locale keys: notes.folders.actions, renameFailed, itemCount,
  itemCountOne, select, bulkSelected, bulkDeleteTitle, bulkDeleteConfirm,
  bulkDeleteFailed.
- Verified: tsc --noEmit clean; all 8 tests in notes-folders,
  notes-card-touch-actions, notes-checklist, notes-delete-confirm pass.
2026-05-07 08:19:50 +00:00
riyadhafraa 25f624f8d8 Task #429: folders-as-files browser at "all" notes view
- Notes reverted to original sticky-note cards (Section + NoteCard) at all
  views, removing the file-manager-style note rendering from task #427.
- Folders now render as a file-manager browser at the top of the "All notes"
  view: big black folder icon, name, relative time, item-count badge, and a
  "…" kebab with rename/delete. Grid and list layouts honor the existing
  view-toggle, plus a "{{count}} folders" footer.
- Added `mode: "rail" | "chips-only"` prop to FoldersRail. At "all" view the
  rail switches to chips-only (All / Unfiled / + New folder) so folder rows
  live exclusively in the new browser and test ids never collide.
- View-toggle is now hidden when not at "all" (folders-only control).
- New FoldersBrowser/FolderItem reuse useUpdateFolder/useDeleteFolder, set
  data-folder-drop for the touch DnD pipeline, and stop click propagation
  inside the dropdown so rename/delete don't navigate the card.
- Added locale keys: notes.folders.actions/renameFailed/itemCount(One).
- Removed obsolete FilesView/FileItem/SelectionBar/DeleteDialog/colorTint
  helpers (~650 lines).
- Verified: tsc --noEmit clean; notes-folders, notes-card-touch-actions,
  notes-checklist, notes-delete-confirm Playwright suites all green.
2026-05-07 08:14:23 +00:00
riyadhafraa 097baddddb Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: eaceab9d-0e1b-4aa6-ab56-3e69031fcc59
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/5kzMIgG
Replit-Helium-Checkpoint-Created: true
2026-05-07 07:59:36 +00:00
riyadhafraa 1676a54ff8 Add file manager view to notes section with grid and list options
Adds new translation keys for UI elements and implements a view mode toggle (grid/list) for the notes page, persisting the selection in local storage.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 88b40c3d-8ff3-492f-891b-c2b0853ab41d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/5kzMIgG
Replit-Helium-Checkpoint-Created: true
2026-05-07 07:52:50 +00:00
riyadhafraa fc14ca4c01 Task #427: fix floating note popup hanging on touch devices
Root cause on iOS Safari (iPad/phone): the `IncomingNotePopup`
rendered a `fixed inset-0 z-[110]` full-viewport wrapper. Stacking
that layer on top of the upcoming-meeting alert plus a Radix toast
intermittently caused the next finger tap to be swallowed — the user
saw the popup "hang". A simultaneous re-render storm from
synchronous query invalidations on multi-event socket fanouts made
it worse on slower hardware.

Fixes:
- Drop the full-viewport wrapper: render the popup card directly
  as a sized `fixed` element. No more invisible viewport-sized
  layer between touch and the rest of the UI.
- Suppress the redundant note/reply toast when the floating
  popup actually accepts the event. The popup IS the surface;
  doubling up just stacks one more floating layer.
- Remove `autoFocus` on the Reply button — focus-steal was
  contributing to the perceived hang.
- Coalesce `react-query` `invalidateQueries` calls in the
  notifications socket: collect into a Set and flush once per
  animation frame so a burst of socket events triggers a single
  refetch pass instead of N synchronous re-renders. Cleaned up
  on socket disconnect.

Tests (artifacts/tx-os/tests/notes-popup-touch-tap.spec.mjs):
- "recipient on a touch device can tap Reply on the floating
  note popup" — verifies the popup is no longer viewport-sized,
  the duplicate toast is suppressed, and a real `locator.tap()`
  succeeds.
- "popup + meeting alert simultaneously: tap on popup still
  works" — the user's exact scenario: seeds an imminent meeting
  so the alert appears, triggers a real cross-user note
  delivery so both float simultaneously, then taps Reply on
  the popup and confirms it dismisses while the alert remains.

Both tests + tsc --noEmit clean.

Files:
- artifacts/tx-os/src/components/notes/incoming-note-popup.tsx
- artifacts/tx-os/src/hooks/use-notifications-socket.ts
- artifacts/tx-os/tests/notes-popup-touch-tap.spec.mjs (new)
2026-05-06 11:54:13 +00:00
riyadhafraa e283a83162 Task #427: fix floating note popup hanging on touch devices
Root cause on iOS Safari (iPad/phone): the `IncomingNotePopup`
rendered a `fixed inset-0 z-[110]` full-viewport wrapper with
`pointer-events-none`. Stacking that layer on top of the upcoming
meeting alert plus a Radix toast intermittently caused the next
finger tap to be swallowed — the user saw the popup "hang".

Fixes:
- Drop the full-viewport wrapper: render the popup card directly
  as a sized `fixed` element. No more invisible viewport-sized
  layer between touch and the rest of the UI.
- Suppress the redundant note/reply toast when the floating
  popup actually accepts the event. The popup IS the surface;
  doubling up just stacks one more floating layer.
- Remove `autoFocus` on the Reply button. A new popup arriving
  while the user is mid-tap would steal focus, contributing to
  the perceived hang.

Tests:
- New `notes-popup-touch-tap.spec.mjs`: emulates a touch viewport,
  sends a real cross-user note, verifies popup bounding box is
  card-sized (not viewport-sized), the redundant toast is
  suppressed, and a `locator.tap()` on Reply dismisses the
  popup and navigates.
- `tsc --noEmit` clean.

Files:
- artifacts/tx-os/src/components/notes/incoming-note-popup.tsx
- artifacts/tx-os/src/hooks/use-notifications-socket.ts
- artifacts/tx-os/tests/notes-popup-touch-tap.spec.mjs (new)
2026-05-06 11:43:23 +00:00
riyadhafraa 9937cb1b08 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 17d62187-995b-45bd-98b9-6f10e3ed6096
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/XE1wdRt
Replit-Helium-Checkpoint-Created: true
2026-05-06 11:25:48 +00:00
riyadhafraa 32db433581 notes: show note-card actions on touch devices (task #425)
Original task: on phones and iPad the per-card action row (color,
label, send, archive, delete) was hidden by `opacity-0
group-hover:opacity-100`. Touch devices have no hover, so users
couldn't send a freshly-created note (or change its color, label,
archive it).

Changes
- artifacts/tx-os/src/pages/notes.tsx
  - Add Tailwind v4 arbitrary variant `[@media(hover:none)]:opacity-100`
    to the per-card action row so it stays always-visible on touch
    devices while desktops keep the existing hover-reveal behaviour.
  - Apply the same variant to the unpinned pin button (which uses the
    identical hover-only treatment).

Tests
- artifacts/tx-os/tests/notes-card-touch-actions.spec.mjs (new)
  - Uses `test.use({ hasTouch: true, isMobile: true, viewport })` on
    chromium (webkit isn't installed) so the page reports
    `(hover: none)`. Sanity-checks that via matchMedia, then creates a
    note via the composer (tap-outside auto-save) and asserts both
    that the Send button is visible and that the action row's
    computed opacity is exactly 1 — Playwright's `toBeVisible` alone
    doesn't catch opacity-0.

Verification
- pnpm --filter @workspace/tx-os exec tsc --noEmit: clean.
- pnpm --filter @workspace/tx-os test:e2e -- notes-delete-confirm
  notes-card-touch-actions: 3 passed (touch test + the two
  delete-confirm specs from #423 to confirm no regression).
- Architect code review: no severe issues. Desktop hover-reveal is
  preserved because `(hover: none)` doesn't match on pointer devices.

Out of scope (per task plan): the trash button inside the
manage-labels dialog (different surface, dialog-only).
2026-05-06 10:39:59 +00:00
riyadhafraa 2027ee2545 notes: replace native confirm() with styled AlertDialog (task #423)
Original task: deleting a note (and a label from the manage-labels menu)
showed the browser's native confirm() with the raw Repl hostname,
clashing with the rest of the app. Switch both call sites to the
project's existing AlertDialog component, matching the pattern used in
FoldersRail and SendNoteDialog.

Changes
- artifacts/tx-os/src/pages/notes.tsx
  - NoteCard: add `confirmDeleteOpen` state; render AlertDialog as a
    Fragment sibling of the clickable card (not a child) so React's
    portal event bubbling cannot trigger the card's onEdit handler when
    the user interacts with the dialog. Also stopPropagation on the
    trash button itself.
  - LabelsDialog: add `pendingDeleteLabel` state and a sibling
    AlertDialog inside the Dialog root. Both dialogs use existing
    locale keys (notes.deleteConfirm, notes.deleteLabelConfirm,
    notes.delete, common.cancel) — no new strings needed.
  - Add data-testids: note-card-delete-{id}, note-delete-dialog-{id},
    note-delete-confirm-{id}, label-delete-{id},
    label-delete-dialog-{id}, label-delete-confirm-{id},
    notes-manage-labels-open.

Tests
- artifacts/tx-os/tests/notes-delete-confirm.spec.mjs (new)
  - Note delete: cancel keeps the card, confirm fires DELETE and
    removes the card. Deliberately does NOT register a `dialog` event
    handler — a native confirm would hang the test.
  - Label delete: open manage-labels, create a label via the UI, click
    trash, confirm, assert DELETE /api/note-labels/{id}.

Verification
- pnpm --filter @workspace/tx-os exec tsc --noEmit: clean.
- pnpm --filter @workspace/tx-os test:e2e -- notes-delete-confirm:
  2 passed.
- Architect code review surfaced a real bubble-through risk on the
  card's onClick={onEdit}; fixed by lifting the AlertDialog to a
  Fragment sibling.

Out of scope (not changed): native confirm()/alert() outside the Notes
page. (executive-meetings.tsx already uses an in-app `confirm({...})`
modal, not the native browser dialog.)
2026-05-06 10:30:28 +00:00
riyadhafraa e82a2b83c2 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: fb93ed69-be42-4dc9-a584-41c32f35d212
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/XE1wdRt
Replit-Helium-Checkpoint-Created: true
2026-05-06 10:19:47 +00:00
riyadhafraa 1ffb470e8f notes: add per-note checklist (to-do list) option
Task #420 — answers the user request "وين خيار اضيف list to do?".

Schema (lib/db/src/schema/notes.ts):
- notes + note_recipients gain `kind` (varchar(16) default 'text')
  and `items` (jsonb<ChecklistItem[]> nullable). Snapshot copy on
  note_recipients keeps delivered checklists immutable across sender
  edits/deletes.
- Drizzle push applied; lib/db .d.ts rebuilt.

API (artifacts/api-server/src/routes/notes.ts):
- ChecklistItem type + bounded parser (≤200 items, id ≤64, text ≤500).
- parseNoteInput normalizes items↔kind on create and on PATCH that
  carries `kind`; PATCH handler additionally coerces items-only
  patches against the note's existing kind so a text note can never
  end up with checklist items (and vice versa).
- POST/PATCH/send/loadRecipientsForNote/received/thread responses and
  the realtime `note_received` payload all carry kind+items, with the
  thread response falling back to the recipient snapshot.

Client (artifacts/tx-os/src/lib/notes-api.ts + pages/notes.tsx):
- Note/ReceivedNote/NoteThread/SentNoteRecipient extended with
  kind+items.
- New `ChecklistEditor`, `ChecklistView`, and `KindToggle` components.
- Composer and EditNoteDialog gain the to-do toggle (ListTodo icon)
  and switch between Textarea and ChecklistEditor; saves send
  kind+items, with empty-checklist auto-discard mirroring the existing
  empty-text behaviour.
- NoteCard, inbox list, sent list, and ThreadDialog body render the
  checklist (read-only on snapshots; owner cards can toggle done via
  PATCH with stopPropagation so the edit dialog doesn't open).

i18n: notes.checklist.{toggle,addItem,itemPlaceholder,emptyHint,
removeItem,progress} added to en.json + ar.json.

Tests: new artifacts/tx-os/tests/notes-checklist.spec.mjs covers
composer→persist→reload→toggle, items-only PATCH normalization on
both kinds, and checklist delivery to recipient snapshot. All 3 pass.

Architect review (evaluate_task) flagged one real issue: items-only
PATCH normalization. Fixed in the PATCH handler and locked in by the
new normalization test.

Pre-existing executive-meetings.ts tsc errors are unchanged and
unrelated to this task.
2026-05-06 10:12:52 +00:00
riyadhafraa d4e0dfc6e5 notes: persist chosen color (and labels) when composer auto-saves
When creating a new note, picking a color from the palette had no
effect — the note saved with the default white background. Editing an
existing note worked because that path lives in a Dialog with no
auto-save guard.

Root cause
The inline Composer in artifacts/tx-os/src/pages/notes.tsx attaches a
document-level mousedown listener that auto-saves whenever the click
falls outside its container ref. The color and label menus are built
on Radix Popover, which renders content in a portal outside the
composer's DOM tree. So clicking a swatch fired:
  1. document mousedown -> save() with the OLD color
  2. swatch click       -> setColor(new) (state already discarded)

Fix
The mousedown handler now ignores clicks whose target is inside any
Radix popper wrapper, Radix portal, or role="dialog" element. The
guard is narrow: legitimate outside clicks (page background, sidebar,
other notes) still trigger save() exactly as before. The same fix
covers the LabelMenu popover for free since it shares the abstraction.

Tests
- New tests/notes-composer-color.spec.mjs reproduces the original
  failure mode: open composer, type title, pick the "red" swatch from
  the popover, click outside, assert the POST /api/notes response has
  color === "red".
- Existing notes-folders + new spec run green together (3 passed in
  29.1s); tx-os tsc --noEmit clean.

Architect notes (followed inline / deferred)
- Suggested mirroring a labels-popover e2e — same code path is already
  exercised; can be added later if regressions surface.
- composedPath() hardening — current target.closest() works for all
  real Radix portals; not needed.
2026-05-06 09:59:10 +00:00
riyadhafraa e96d709bb1 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 70f56bc6-af8d-4b43-afd8-10d93fe8cfb1
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/uqawMO0
Replit-Helium-Checkpoint-Created: true
2026-05-06 09:54:38 +00:00
riyadhafraa 42e57e6e0b Add a confirmation dialog before deleting a folder
Integrates an AlertDialog component for folder deletion, replacing the native confirm dialog, and updates corresponding locale files and test cases.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 6a869b6f-2c2b-4d44-aab0-25864af19415
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/uqawMO0
Replit-Helium-Checkpoint-Created: true
2026-05-06 09:29:46 +00:00
riyadhafraa 3a8590cf8d Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 554658ce-6ac7-43ce-b72c-857eca56a28b
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/uqawMO0
Replit-Helium-Checkpoint-Created: true
2026-05-06 09:25:10 +00:00
riyadhafraa 466a8c2340 Cap note thread dialog height (task #417)
User reported (in Arabic, with screenshot) that the note Replies dialog was
covering the entire page when a thread had multiple grouped conversations.
The owner/admin grouped-replies view (`GroupedReplies`) had a `max-h-72`
internal scroll, but the outer `DialogContent` had no overall height cap, so
on tall content the dialog grew to fill the viewport.

Changes
- artifacts/tx-os/src/pages/notes.tsx
  - `DialogContent` for the thread dialog: add `max-h-[85vh] flex flex-col`.
  - Refactored body into three vertical zones (per code-review feedback):
    1) static meta + recipient chips (`shrink-0`, always visible),
    2) replies-only scroll region (`flex-1 min-h-0 overflow-y-auto`,
       `data-testid="note-thread-scroll"`),
    3) static composer (`shrink-0`, pinned at the bottom).
  - Drop now-redundant `max-h-48` on the recipient ReplyBubble list and
    `max-h-72` on `GroupedReplies` — the dedicated scroll zone handles
    overflow, avoiding nested double scrollbars.

Tests
- New artifacts/tx-os/tests/notes-thread-dialog.spec.mjs:
  - Seeds an owner note with 2 recipients and 30 alternating replies via
    SQL, deep-links into `/notes?thread=ID`, asserts the dialog's bounding
    box height ≤ 0.9 × viewport height, asserts the inner scroll region
    actually overflows (scrollHeight > clientHeight), and asserts the
    composer remains visible.
- Verified the existing notes-folders.spec.mjs (2 tests) still pass.
- `tsc --noEmit` clean.

Out of scope (left as-is): visual redesign, reply send/archive logic,
Inbox/Sent thread list.
2026-05-06 08:55:00 +00:00
riyadhafraa 889e24244c Cap note thread dialog height (task #417)
User reported (in Arabic, with screenshot) that the note Replies dialog was
covering the entire page when a thread had multiple grouped conversations.
The owner/admin grouped-replies view (`GroupedReplies`) had a `max-h-72`
internal scroll, but the outer `DialogContent` had no overall height cap, so
on tall content the dialog grew to fill the viewport.

Changes
- artifacts/tx-os/src/pages/notes.tsx
  - `DialogContent` for the thread dialog: add `max-h-[85vh] flex flex-col`.
  - Wrap the middle (sender info + recipients chips + replies block) in a
    new `flex-1 min-h-0 overflow-y-auto` scroller with
    `data-testid="note-thread-scroll"`.
  - Pin the reply composer with `shrink-0` so it stays visible while the
    middle scrolls.
  - Drop now-redundant `max-h-48` on the recipient ReplyBubble list and
    `max-h-72` on `GroupedReplies` — the outer scroller handles overflow,
    avoiding nested double scrollbars.

Tests
- New artifacts/tx-os/tests/notes-thread-dialog.spec.mjs:
  - Seeds an owner note with 2 recipients and 30 alternating replies via
    SQL, deep-links into `/notes?thread=ID`, asserts the dialog's bounding
    box height ≤ 0.9 × viewport height, asserts the inner scroll region
    actually overflows (scrollHeight > clientHeight), and asserts the
    composer remains visible.
- Verified the existing notes-folders.spec.mjs (2 tests) still pass.
- `tsc --noEmit` clean.

Out of scope (left as-is): visual redesign, reply send/archive logic,
Inbox/Sent thread list.
2026-05-06 08:52:27 +00:00
riyadhafraa 01dfaccb53 Transitioned from Plan to Build mode
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: f51fcd22-676f-4c0f-986c-ce2839616b04
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/uqawMO0
Replit-Helium-Checkpoint-Created: true
2026-05-06 08:48:23 +00:00
riyadhafraa 60c23d4972 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 08:09:59 +00:00
riyadhafraa 10694f8d42 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 08:07:43 +00:00
riyadhafraa 6e49010940 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 08:05:12 +00:00
riyadhafraa b976c408a6 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 08:02:10 +00:00
riyadhafraa f562749214 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 07:57:57 +00:00
riyadhafraa 1889407565 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 07:55:32 +00:00
riyadhafraa 9fd4671327 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 07:52:54 +00:00
riyadhafraa 6826b475b2 notes: user-defined folders with drag-drop (task #413)
DB
- New `note_folders` table (per-user unique name); `notes.folder_id` nullable
  with `ON DELETE SET NULL` so deleting a folder preserves its notes.

API
- /note-folders CRUD (GET, POST, PATCH, DELETE), all user-scoped.
- /notes POST/PATCH validate folder ownership before assignment (no cross-user
  folder bleed).
- Folder noteCount is tab-aware (active vs archived) via ?archived=.
- OpenAPI spec extended with /note-folders endpoints, NoteFolder schema, and
  folderId on SentNote/ReceivedNote. Codegen regenerated for api-client-react
  and api-zod (`pnpm --filter @workspace/api-spec run codegen`).

Client
- NoteFolder type and useNoteFolders / useCreateFolder / useUpdateFolder /
  useDeleteFolder / useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches; rollback on error).

UI (FoldersRail next to My Notes + Archive tabs only)
- Desktop: sidebar with All / Unfiled / user folders + counts.
- Mobile: horizontal chip row (flex + overflow-x-auto) above the notes grid.
- Per-folder kebab dropdown with Rename + Delete.
- Newly-created folder is auto-selected.
- HTML5 draggable note cards on desktop; touch long-press fallback (350ms)
  for iPad/mobile via shared drop pipeline (registerFolderDropHandler +
  hit-tested `[data-folder-drop]`).
- Dev-only `window.__notesTouchTest` helper (gated to non-production builds)
  exposes the touch pipeline so e2e tests can drive it deterministically
  without synthesizing native TouchEvents.

Bilingual
- notes.folders.* keys added to en.json and ar.json (RTL inherited from
  existing dir prop wired from i18n.language).

Tests (artifacts/tx-os/tests/notes-folders.spec.mjs)
- create folder + auto-select
- drag note → folder, refresh-and-still-in-folder persistence
- filter by folder / Unfiled / All
- drag between folders (count math)
- rename via kebab
- delete a non-empty folder via kebab → notes return to Unfiled (FK SET NULL)
- cross-user folder rejection (400)
- Arabic + RTL render
- separate touch test (414×800, hasTouch+isMobile) covers chip-row layout
  and exercises the touch drop pipeline through window helper.

All API node tests + tx-os Playwright suite + tsc clean.
2026-05-06 07:50:39 +00:00
riyadhafraa 0bcfb9017d notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on
  notes with ON DELETE SET NULL.
- API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate
  folder ownership; folder noteCount is tab-aware (active vs archived) via
  ?archived= query.
- Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/
  useDeleteFolder/useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches).
- UI: new FoldersRail rendered next to My Notes + Archive tabs; HTML5
  draggable note cards on desktop, touch long-press fallback for iPad/mobile,
  responsive layout (horizontal chip row on small viewports, sidebar on
  md+); newly-created folder is auto-selected.
- Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works
  via existing dir prop wired from i18n.language).
- Test: tests/notes-folders.spec.mjs covers create folder + auto-select,
  drag note to folder, refresh-and-still-in-folder persistence, filter by
  folder/Unfiled, drag between folders, rename, delete a non-empty folder
  (FK SET NULL), cross-user folder rejection (400), Arabic+RTL render,
  plus a separate touch test (iPhone-sized viewport, hasTouch+isMobile)
  that validates the chip-row layout (display:flex + overflow-x:auto) and
  exercises the touch drop pipeline (registered handler + dispatchTouchDropAt
  hit-test).

Drift from plan: none. Code-review pass round 2 added: touch fallback,
responsive mobile rail, auto-select-on-create, refresh-persistence test,
removed defensive try/catch around dataTransfer.
2026-05-06 07:46:33 +00:00
riyadhafraa 2b01cc1c24 notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on
  notes with ON DELETE SET NULL.
- API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate
  folder ownership; folder noteCount is tab-aware (active vs archived) via
  ?archived= query.
- Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/
  useDeleteFolder/useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches).
- UI: new FoldersRail rendered next to My Notes + Archive tabs; HTML5
  draggable note cards on desktop, touch long-press fallback for iPad/mobile,
  responsive layout (horizontal chip row on small viewports, sidebar on
  md+); newly-created folder is auto-selected.
- Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works
  via existing dir prop wired from i18n.language).
- Test: tests/notes-folders.spec.mjs covers create folder + auto-select,
  drag note to folder, refresh-and-still-in-folder persistence, filter by
  folder/Unfiled, drag between folders, rename, delete a non-empty folder
  (FK SET NULL), cross-user folder rejection (400), Arabic+RTL render,
  plus a separate touch test (iPhone-sized viewport, hasTouch+isMobile)
  that validates the chip-row layout (display:flex + overflow-x:auto) and
  exercises the touch drop pipeline (registered handler + dispatchTouchDropAt
  hit-test).

Drift from plan: none. Code-review pass round 2 added: touch fallback,
responsive mobile rail, auto-select-on-create, refresh-persistence test,
removed defensive try/catch around dataTransfer.
2026-05-06 07:43:31 +00:00
riyadhafraa 4d720fdeed notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on
  notes with ON DELETE SET NULL.
- API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate
  folder ownership; folder noteCount is tab-aware (active vs archived) via
  ?archived= query.
- Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/
  useDeleteFolder/useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches).
- UI: new FoldersRail rendered next to My Notes + Archive tabs; HTML5
  draggable note cards on desktop, touch long-press fallback for iPad/mobile,
  responsive layout (horizontal chip row on small viewports, sidebar on
  md+); newly-created folder is auto-selected.
- Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works
  via existing dir prop wired from i18n.language).
- Test: tests/notes-folders.spec.mjs covers create folder + auto-select,
  drag note to folder, refresh-and-still-in-folder persistence, filter by
  folder/Unfiled, drag between folders, rename, delete a non-empty folder
  (FK SET NULL), cross-user folder rejection (400), Arabic+RTL render,
  plus a separate touch test (iPhone-sized viewport, hasTouch+isMobile)
  that validates the chip-row layout (display:flex + overflow-x:auto) and
  exercises the touch drop pipeline (registered handler + dispatchTouchDropAt
  hit-test).

Drift from plan: none. Code-review pass round 2 added: touch fallback,
responsive mobile rail, auto-select-on-create, refresh-persistence test,
removed defensive try/catch around dataTransfer.
2026-05-06 07:41:22 +00:00
riyadhafraa fd194c5ed1 notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on
  notes with ON DELETE SET NULL.
- API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate
  folder ownership; folder noteCount is tab-aware (active vs archived) via
  ?archived= query.
- Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/
  useDeleteFolder/useMoveNoteToFolder hooks (optimistic across all
  ["notes", ...] caches).
- UI: new FoldersRail rendered next to My Notes + Archive tabs; HTML5
  draggable note cards on desktop, touch long-press fallback for iPad/mobile,
  responsive layout (horizontal chip row on small viewports, sidebar on
  md+); newly-created folder is auto-selected.
- Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works
  via existing dir prop wired from i18n.language).
- Test: tests/notes-folders.spec.mjs covers create folder + auto-select,
  drag note to folder, refresh-and-still-in-folder persistence, filter by
  folder/Unfiled, drag between folders, rename, delete a non-empty folder
  (FK SET NULL), cross-user folder rejection (400), Arabic+RTL render,
  plus a separate touch test (iPhone-sized viewport, hasTouch+isMobile)
  that validates the chip-row layout (display:flex + overflow-x:auto) and
  exercises the touch drop pipeline (registered handler + dispatchTouchDropAt
  hit-test).

Drift from plan: none. Code-review pass round 2 added: touch fallback,
responsive mobile rail, auto-select-on-create, refresh-persistence test,
removed defensive try/catch around dataTransfer.
2026-05-06 07:39:03 +00:00
riyadhafraa ba0da3d26c notes: user-defined folders with drag-drop (task #413)
- DB: new note_folders table (per-user unique name) + nullable folder_id on
  notes with ON DELETE SET NULL; pushed via @workspace/db.
- API: full /note-folders CRUD with user scoping; /notes POST/PATCH validate
  folder ownership; folder noteCount is tab-aware (active vs archived) via
  ?archived= query param computed by GROUP BY (drizzle correlated subquery
  was returning 0 — replaced with two queries merged in JS).
- Client: NoteFolder type, useNoteFolders/useCreateFolder/useUpdateFolder/
  useDeleteFolder/useMoveNoteToFolder hooks (optimistic update for moves
  across all ["notes", ...] caches).
- UI: new FoldersRail (artifacts/tx-os/src/components/notes/folders-rail.tsx)
  rendered next to My Notes + Archive tabs; HTML5 draggable note cards with
  module-scoped DRAGGING_NOTE_ID fallback; visible drop highlight; rename +
  delete + create inline; folder-scoped + Unfiled filtering in notes.tsx.
- Bilingual: notes.folders.* keys added to en.json and ar.json (RTL works
  via existing dir prop wired from i18n.language).
- Test: artifacts/tx-os/tests/notes-folders.spec.mjs covers create folder,
  drag note to folder, filter by folder/Unfiled, drag between folders,
  rename, delete a non-empty folder (verifies FK SET NULL + Unfiled count),
  cross-user folder rejection (400), and Arabic+RTL rendering.

Drift from plan: none — all originally scoped work shipped. Strengthened
e2e + cross-user check + tab-aware counts added per code review feedback.
2026-05-06 07:26:11 +00:00