Original task: On the Notes page the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below. Composer appeared high, rail
visibly lower with empty space between them.
Change:
- Switched the folders+content row in
`artifacts/tx-os/src/pages/notes.tsx` from flex to CSS grid:
`grid md:grid-cols-[14rem_minmax(0,1fr)]`. Composer is placed in
col 2 row 1, FoldersRail in col 1 (row-span-2 when composer is
shown), content in col 2 row 2. Result: composer's top edge aligns
with the rail's top edge on desktop and the empty gap is gone.
- On mobile (`grid-cols-1`), `order-1 / order-2 / order-3` give the
exact original mobile reading order: composer → rail → content.
- Added a `className?: string` prop to `FoldersRail`
(`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended
to its root `<aside>`, so the page can pass grid placement
classes.
- Hidden-composer views (Inbox / Sent / Archived / Shared-with-me)
collapse cleanly via conditional `md:row-span-2` on the rail and
conditional `md:row-start-1 / md:row-start-2` on the content
column — desktop becomes a simple two-cell row and mobile stays
rail → content, matching the original behavior.
- Grid-cols template, `md:col-start-2` on composer, and
`md:col-start-2`/`md:row-start-*` on the content column are all
GATED on `showFolders`. In views where the folders rail is hidden
(Received/Inbox, Sent), the wrapper is a single-column grid with
no left gutter and content takes the full width — preserving the
original full-width layout for those views.
Behavior preserved:
- `showTopComposer` gating unchanged.
- `data-testid="notes-top-composer"` preserved.
- FoldersRail's existing internal classes unchanged; new className
prop is purely additive.
- Mobile reading order in active/non-shared view: composer → rail →
content (same as before this task, when composer was a standalone
block above the row).
- Mobile reading order in hidden-composer views: rail → content
(unchanged).
Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit`
passes. No test files reference the composer block by structural
selectors that the move would break.
No deviations from the plan.
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below. Composer appeared high, rail
visibly lower with empty space between them.
Change:
- Switched the folders+content row in
`artifacts/tx-os/src/pages/notes.tsx` from flex to CSS grid:
`grid md:grid-cols-[14rem_minmax(0,1fr)]`. Composer is placed in
col 2 row 1, FoldersRail in col 1 (row-span-2 when composer is
shown), content in col 2 row 2. Result: composer's top edge aligns
with the rail's top edge on desktop and the empty gap is gone.
- On mobile (`grid-cols-1`), `order-1 / order-2 / order-3` give the
exact original mobile reading order: composer → rail → content.
- Added a `className?: string` prop to `FoldersRail`
(`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended
to its root `<aside>`, so the page can pass grid placement
classes.
- Hidden-composer views (Inbox / Sent / Archived / Shared-with-me)
collapse cleanly via conditional `md:row-span-2` on the rail and
conditional `md:row-start-1 / md:row-start-2` on the content
column — desktop becomes a simple two-cell row and mobile stays
rail → content, matching the original behavior.
Behavior preserved:
- `showTopComposer` gating unchanged.
- `data-testid="notes-top-composer"` preserved.
- FoldersRail's existing internal classes unchanged; new className
prop is purely additive.
- Mobile reading order in active/non-shared view: composer → rail →
content (same as before this task, when composer was a standalone
block above the row).
- Mobile reading order in hidden-composer views: rail → content
(unchanged).
Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit`
passes. No test files reference the composer block by structural
selectors that the move would break.
No deviations from the plan.
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below. Composer appeared high, rail
visibly lower with empty space between them.
Change:
- Moved the top composer out of its standalone full-width block and
into the left content column (`flex-1 min-w-0`) of the existing
folders+content row in `artifacts/tx-os/src/pages/notes.tsx`. Both
now share the same row, so the composer's top edge aligns with the
rail's top edge on desktop.
- Added a `className?: string` prop to `FoldersRail`
(`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended
to its root <aside>, so the page can pass `order-*` classes.
- On narrow widths (`flex-col`) the composer column gets
`order-1 md:order-2` and the rail gets `order-2 md:order-1` ONLY
when `showTopComposer` is true. In hidden-composer views (Inbox /
Sent / Archived / Shared-with-me) no order classes are applied, so
the rail/content stacking order on mobile stays exactly as it was
before this task. Result: composer stacks ABOVE the rail on mobile
in My Notes (preserving the original mobile reading order) and
sits beside it on md+, with zero behavior change in the other
views.
Behavior preserved:
- `showTopComposer` gating unchanged — still hidden on Inbox / Sent /
Archived / Shared-with-me.
- `data-testid="notes-top-composer"` preserved.
- FoldersRail's existing width/scroll classes unchanged; new className
is purely additive.
- Stale comment about composer being "above the folders area" updated
to describe the new in-column placement and the order-* mobile
behavior.
Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit`
passes. No test files reference the composer block by structural
selectors that the move would break.
No deviations from the plan.
Original task: On the Notes page the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below. Composer appeared high, rail
visibly lower with empty space between them.
Change:
- Moved the top composer out of its standalone full-width block and
into the left content column (`flex-1 min-w-0`) of the existing
folders+content row in `artifacts/tx-os/src/pages/notes.tsx`. Both
now share the same row, so the composer's top edge aligns with the
rail's top edge on desktop.
- Added a `className?: string` prop to `FoldersRail`
(`artifacts/tx-os/src/components/notes/folders-rail.tsx`) appended
to its root <aside>, so the page can pass `order-*` classes.
- On narrow widths (`flex-col`) the composer column gets
`order-1 md:order-2` and the rail gets `order-2 md:order-1`. Result:
composer stacks ABOVE the rail on mobile (preserving the original
mobile reading order) and sits beside it on md+.
Behavior preserved:
- `showTopComposer` gating unchanged — still hidden on Inbox / Sent /
Archived / Shared-with-me.
- `data-testid="notes-top-composer"` preserved.
- FoldersRail's existing width/scroll classes unchanged; new className
is purely additive.
- Stale comment about composer being "above the folders area" updated
to describe the new in-column placement and the order-* mobile
behavior.
Verification: `pnpm --filter @workspace/tx-os exec tsc --noEmit`
passes. No test files reference the composer block by structural
selectors that the move would break.
No deviations from the plan.
Original task: On the Notes page, the "اكتب ملاحظة" composer sat alone
in a full-width row above the content row, while the "المجلدات"
FoldersRail started in the row below it. The result was a visible
vertical mismatch — composer high, rail noticeably lower with empty
space between them.
Change: Moved the top composer out of its standalone full-width block
and into the left content column (`flex-1 min-w-0`) at the top of the
existing row that already hosts the FoldersRail. Both now share the
same row, so the composer's top edge aligns with the rail's top edge.
Details:
- Single edit in `artifacts/tx-os/src/pages/notes.tsx`.
- The composer's `data-testid="notes-top-composer"` is preserved, so
any existing E2E selectors continue to work.
- `showTopComposer` gating (hide on Inbox/Sent/Archived/Shared-with-me)
is unchanged — only the position of the rendered block moved.
- Removed the old wrapper's `px-4 pt-1 max-w-6xl w-full mx-auto`
(those paddings/centering are now provided by the parent row), and
added a `mb-4` for breathing room between composer and the content
below it inside the column.
- Responsive behavior preserved: the parent row is
`flex-col md:flex-row`, so on narrow widths the composer still
stacks above the folders area cleanly with no horizontal overflow.
Verification: pnpm tsc --noEmit on tx-os passes; no test files
reference the composer block by structural selectors that the move
would break.
No deviations from the plan.
Adds a recipient-scoped delete that's distinct from Archive: a recipient
can remove a note from their own inbox without touching the underlying
note or other recipients' rows.
Backend (artifacts/api-server/src/routes/notes.ts):
- DELETE /notes/received/:id — recipient-only; 404 if no row.
- POST /notes/received/bulk-delete — body {ids:number[]}, max 500,
single SQL DELETE, returns {ok, notFound} for partial-success UI.
- Both registered before DELETE /notes/:id so Express matches /received
first.
Frontend (artifacts/tx-os):
- New hooks useDeleteReceivedNote / useBulkDeleteReceivedNotes in
src/lib/notes-api.ts; both invalidate notes + folders queries.
- Inbox bulk bar: Delete button (rose) next to Archive plus a confirm
AlertDialog with all/none/partial toasts.
- Inbox card: per-row Trash button next to the status badge with a
page-level confirm AlertDialog (testid inbox-row-delete-confirm).
- ThreadDialog: per-row Delete next to Archive plus a confirm dialog
that closes the thread on success.
- AR + EN locale strings added for all new copy.
Tests:
- artifacts/api-server/tests/notes-inbox-delete.test.mjs — recipient
delete, non-recipient/sender 404, bulk mix of valid+missing ids, and
DB-level checks that the note + other recipients survive.
- artifacts/tx-os/tests/notes-inbox-bulk-delete.spec.mjs — Playwright
flow seeds three received notes, bulk-deletes two from the inbox,
then deletes the third via the per-row card Delete button.
Adds a recipient-scoped delete that's distinct from Archive: a recipient
can remove a note from their own inbox without touching the underlying
note or other recipients' rows.
Backend (artifacts/api-server/src/routes/notes.ts):
- DELETE /notes/received/:id — recipient-only; 404 if no row.
- POST /notes/received/bulk-delete — body {ids:number[]}, max 500,
single SQL DELETE, returns {ok, notFound} for partial-success UI.
- Both registered before DELETE /notes/:id so Express matches /received
first.
Frontend (artifacts/tx-os):
- New hooks useDeleteReceivedNote / useBulkDeleteReceivedNotes in
src/lib/notes-api.ts; both invalidate notes + folders queries.
- Inbox bulk bar gets a Delete button (rose) next to Archive plus a
confirm AlertDialog with all/none/partial toasts (src/pages/notes.tsx).
- ThreadDialog gets a per-row Delete next to Archive plus a single
confirm dialog that closes the thread on success.
- AR + EN locale strings added for all new copy.
Tests:
- artifacts/api-server/tests/notes-inbox-delete.test.mjs — recipient
delete, non-recipient/sender 404, bulk mix of valid+missing ids, and
DB-level checks that the note + other recipients survive.
- artifacts/tx-os/tests/notes-inbox-bulk-delete.spec.mjs — Playwright
flow seeds three received notes, bulk-deletes two from the inbox,
then deletes the third via ThreadDialog per-row.
User asked for the Edit App modal to match the new professional Edit
Service design and confirmed responsiveness for iPad / mobile.
Changes — admin.tsx only:
1. New shell matching Edit Service modal:
- bg-slate-950/50 backdrop-blur-md overlay, click-outside-to-close,
fade + zoom-in animations.
- White rounded-2xl container, max-w-2xl (wider than Service modal
because of the additional fields + permissions list), max-h-[90vh],
flex column with sticky-feeling header / footer.
- Header tile is now LIVE — its gradient color comes from the
editing app's color field, and the icon comes from the app's
iconName via a new resolveAppIcon() helper. So the header
previews the app as you edit.
- X close button in header.
2. Form layout:
- Names row (sm:grid-cols-2): nameAr (RTL, ع badge) | nameEn (LTR,
EN badge).
- Identifier row (sm:grid-cols-2): slug | route, both font-mono.
- Visuals row (sm:grid-cols-2):
- Icon picker: 40x40 preview tile + text input. Tile shows the
resolved Lucide icon, or a dashed HelpCircle when the name
doesn't resolve. Hint text under the field.
- Color picker: clickable color swatch wrapping a hidden native
<input type=color> + hex text input. Both stay in sync.
- Permissions wrapped in a bordered card with its own header strip.
3. Footer: Cancel (outline) + Save (gradient indigo→blue, shadow),
each flex-1 h-10 rounded-lg.
4. New helper resolveAppIcon() above the imports — does a safe lookup
into the lucide-react namespace and returns null when the name is
invalid. Mirrors the pattern already used in pages/home.tsx.
Responsiveness verified by reasoning over breakpoints:
- Mobile (≤414px): sm: never kicks in, so all rows stack into one
column; outer p-4 keeps gutters; max-h-[90vh] + scroll handles tall
permissions.
- iPad portrait (768px) and up: sm:grid-cols-2 activates, two-column
layout fits comfortably inside max-w-2xl (672px).
- Desktop: same max-w-2xl cap looks well-proportioned, not cramped.
- Edit Service modal already uses the same primitives, no changes
needed.
No behavioral changes — all state wiring (editingApp, handleSaveApp,
permission flows, history sections) is unchanged. Pure presentational
refactor.
Code review: skipped — presentational refactor with no logic / data
flow changes.
Follow-ups: none proposed — request fully addressed; the other admin
modals are deliberately out of scope per the plan.
Refactors the "Edit Service" modal UI in `admin.tsx` to improve visual hierarchy, user experience, and overall professionalism, including styling changes to the header, input fields, image uploader, and availability toggle.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 28ee2705-c2ec-476e-aa4f-cc6c7cd5624e
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/O07Jndv
Replit-Helium-Checkpoint-Created: true
User requested removal of the price field from the admin Services edit
form ("بالنسبه الى الاسعار أحذفها لايوجد اسعار") because there are no
prices in the product.
Changes — frontend only, all in tx-os:
- artifacts/tx-os/src/pages/admin.tsx
- Drop `price: string` from ServiceForm type.
- Drop `price: "0.00"` from emptyServiceForm.
- Remove the price entry from the edit dialog's field array
(no more السعر input).
- Remove the `<div>{service.price}</div>` from the services list.
- Remove the `price` line from the edit-load mapper.
- artifacts/tx-os/src/locales/ar.json + en.json
- Drop the now-unused `servicePrice` key.
Left intact:
- The generic `"price"` key in locales (line 104) — kept conservatively
in case anything outside this scope expects it, though ripgrep shows
no current usage.
- The `services.price` DB column and api-server code — unused in api
src; only test fixtures insert a placeholder. Removing would require
a migration and break tests for zero benefit.
- The user-facing services page — never displayed price.
Verification:
- ripgrep confirms zero `price` / `servicePrice` references remain in
artifacts/tx-os/src/pages/admin.tsx and the locales (other than the
generic shared key).
- Vite HMR applied cleanly with no TS errors.
- Pre-existing test failures (executive-meetings, service-orders) are
unrelated and already tracked as separate project tasks.
Code review: skipped — change is a 5-line mechanical removal of dead
UI with no logic or data-flow implications.
Follow-ups: none proposed — this is a self-contained, complete fix.
User report: after #505 the admin "لوحة التحكم" header looked pinned
at the top of the page, but disappeared as soon as the user scrolled
down. Same bug silently affected every page wrapped in `.os-bg`.
Root cause (artifacts/tx-os/src/index.css, lines 190-193):
.os-bg > * { position: relative; z-index: 1; }
This rule was meant to lift page content above the decorative
gradient blobs (`.os-bg::before/::after`, position:fixed z-index:0).
But by setting `position: relative` on every direct child it also
overrode any `position: sticky` declared via Tailwind utilities on
those children — equal-specificity class selectors, with index.css
loaded after Tailwind utilities, so .os-bg won source-order. The
"sticky" headers therefore behaved as plain in-flow elements and
scrolled away with the rest of the content.
Fix (one CSS rule):
- Replace `position: relative; z-index: 1` with `isolation: isolate`.
- `isolation: isolate` creates a new stacking context (so children
still paint above the z-index:0 blobs) without forcing them to
position:relative — sticky/fixed children now work as written.
No markup changes anywhere. Followed Option A in the task plan
exactly. Implication: every sticky header across the app (admin,
home, chat, services, notifications) is now actually pinned on
scroll. Decorative blobs continue to sit behind page content
because the new stacking context still paints over the parent's
z-index 0 background layer.
No tests were modified; existing tx-os tests are presentation-
agnostic and unaffected.
User report: in the admin dashboard on iPad, the top bar (back arrow,
gear icon, "لوحة التحكم" title, mobile menu button) felt like it
detached from the top during scroll — content showed through it and
it didn't look pinned.
Root cause (artifacts/tx-os/src/pages/admin.tsx, line 1148):
the header was `glass-panel ... sticky top-0 z-10`. The `glass-panel`
background is rgba(255,255,255,0.7) — semi-transparent — and z-10 is
low enough that floating elements on the page (os-bg blobs, sidebar
backdrop) bled across it. The sticky positioning itself was correct;
the perception of "not pinned" came from the see-through background.
Fix (single-file CSS-only change, exactly as planned):
- Drop `glass-panel`, replace with inline styles for full control.
- Background: rgba(255,255,255,0.96) — visually opaque so scrolled
content no longer shows through.
- Keep blur(16px) saturate(160%) (with -webkit- prefix) so the bar
still has the same soft glass aesthetic.
- z-30 instead of z-10 so nothing in the page overlays it. Modals
use z-50 so they still cover the bar correctly.
- Add boxShadow 0 4px 20px rgba(15,23,42,0.06) so the bar visibly
floats above content once scrolling starts.
- Kept `sticky top-0` (not converted to fixed) so the desktop
sidebar's `top-[73px]` offset still aligns with the bar's bottom.
- Header height unchanged (px-4 py-4 + same children).
No layout changes. No other top bars touched (out of scope).
Existing tx-os tests are unaffected (no logic, only presentation).
User report: in the Tx OS launcher, the dot above ج in "الاجتماعات"
(and similar diacritics on other tile labels) was being sliced off on
iPad — a visible visual-quality bug on the most-used screen.
Root cause (artifacts/tx-os/src/pages/home.tsx, AppIconContent):
the label span used `leading-tight` (line-height 1.25) + `truncate`
(overflow: hidden). At 11px the line-box was 13.75px tall — too short
for Arabic ascenders + diacritics — and overflow:hidden then clipped
the parts that overshot. iPad/WebKit rasterizes these faces slightly
taller than desktop, so the clip is most visible there.
Fix (single-file CSS-only change):
- Replace `leading-tight` with `lineHeight: 1.6` for plenty of room
above the baseline.
- Add `paddingTop: 2` as cheap insurance for any remaining overshoot.
- Keep ellipsis truncation behaviour explicitly via inline style
(overflow:hidden + textOverflow:ellipsis + whiteSpace:nowrap), so
long Arabic names still ellipsize at the 78px max-width.
- Add `block` so max-width applies cleanly.
No change to icon tile size, grid layout, badges, or any other
markup. Acceptance items in the task plan all hold:
- Letter dots are no longer clipped.
- Long names still truncate with an ellipsis at 78px.
- Desktop layout is visually unchanged.
Deviation from plan: chose the inline-style escape hatch over
line-clamp-1, because line-clamp uses `display: -webkit-box` which
can interact with `max-width` differently across browsers, and the
explicit overflow/ellipsis form is the closest behavioural match to
the previous `truncate`.
Adds a title tag to the HTML output for PDF generation and refactors the Content-Disposition header to include both a stable ASCII filename and a localized, human-readable filename for PDF downloads.
Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 77cfe984-2c65-4152-bb7a-0df28274fe66
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Event-Id: 7b3c6ef0-56bb-48e7-8f67-40892dcf240d
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/c3c252e4-c83d-40ca-9fff-99a3ea60701e/77cfe984-2c65-4152-bb7a-0df28274fe66/4ugHzxo
Replit-Helium-Checkpoint-Created: true
The notes composer now starts with the yellow swatch pre-selected
instead of the neutral "default" color, so a freshly created note
becomes a yellow sticky by default. The user can still pick any
other color from the swatch picker before saving, and that choice
is honored. After a successful save (or send), the composer resets
back to yellow rather than "default", so the next note in the same
session is also yellow unless the user changes it.
Changes:
- artifacts/tx-os/src/pages/notes.tsx
- Composer initial color state: useState("default") -> useState("yellow")
- Composer reset() helper: setColor("default") -> setColor("yellow")
Out of scope (per task spec):
- No backend changes. The API's data.color = "default" fallback for
notes created via other paths (shared-folder stamping, incoming-
note flows) is intentionally untouched.
- No changes to existing notes already saved in the database.
- No new "default color" user setting.
- NOTE_COLORS palette and color-picker UI unchanged.
Verification:
- `pnpm exec tsc --noEmit` passes clean.
- "yellow" is a valid id in NOTE_COLORS (src/lib/notes-api.ts:801),
so colorBg/colorAccent/colorMeta resolve correctly and the swatch
shows pre-selected.
- Color-picker onChange path unchanged, so user-picked colors still
override the new yellow default before save.
Two scoped UX fixes on Executive Meetings schedule, both confined to
artifacts/tx-os/src/pages/executive-meetings.tsx (shared
src/components/ui/dialog.tsx untouched per task spec).
1) Quick Actions dialog header overlap
- Radix DialogContent renders the Close (X) at physical
`right-4 top-4` regardless of `dir`. In AR (RTL) the
`text-right`-aligned title "إجراءات سريعة" pinned to the same
edge and visibly overlapped the X.
- Fix: added `pr-8` to the per-row Quick Actions DialogHeader so
the title's right edge always clears the close button. Works
for AR (overlap eliminated) and EN (just adds trailing slack).
2) iPad long-press drag feedback
- dnd-kit's TouchSensor has a 200ms activation delay; previously
the row gave no cue during the hold window so users lifted
early thinking the schedule didn't register.
- Fix: added `isPressing` state toggled via capture-phase
onPointerDown/Up/Cancel handlers on the row tr, gated on
`pointerType === "touch"` && `effectiveDragEnabled`. Capture
phase chosen so handlers don't collide with safeRowDragListeners
(which spreads dnd-kit's bubble-phase onPointerDown).
- Visual cue: subtle `inset 0 0 0 2px hexToRgba(highlightColor,
0.45)` ring composed AFTER quickOpen/selection/current rings so
it never outranks a real selection state. Per spec, the cue
appears during the 200ms hold AND continues throughout the
active drag (we deliberately do NOT clear isPressing on the
rising edge of isDragging). Two defensive useEffects clear
`isPressing`: one when drag becomes disabled (capability flip /
edit-mode toggle), one tied to the FALLING edge of isDragging
(drag just ended) as a belt-and-suspenders guard against stale
visual state when dnd-kit absorbs the touch sequence and our
row's own pointerup never fires.
- data-pressing="true" attribute exposed on tr for tests.
- TouchSensor delay (200ms) and tolerance (8px) unchanged.
Tests added:
- executive-meetings-quick-actions-dialog-layout.spec.mjs: asserts
close button + title bounding boxes don't overlap in AR + EN.
- executive-meetings-row-touch-press-feedback.spec.mjs: uses
hasTouch+isMobile + CDP Input.dispatchTouchEvent, asserts
data-pressing flips true on touchstart, clears on touchend, and
tap-to-open quick-actions dialog still works for short taps;
second test verifies the cue persists past the 200ms window.
Verification: pnpm tsc --noEmit clean. Pre-existing `test` workflow
failures (rotate-content/font-settings/notes-share) are unrelated to
this change.
Two scoped UX fixes on Executive Meetings schedule, both confined to
artifacts/tx-os/src/pages/executive-meetings.tsx (shared
src/components/ui/dialog.tsx untouched per task spec).
1) Quick Actions dialog header overlap
- Radix DialogContent renders the Close (X) at physical
`right-4 top-4` regardless of `dir`. In AR (RTL) the
`text-right`-aligned title "إجراءات سريعة" pinned to the same
edge and visibly overlapped the X.
- Fix: added `pr-8` to the per-row Quick Actions DialogHeader so
the title's right edge always clears the close button. Works
for AR (overlap eliminated) and EN (just adds trailing slack).
2) iPad long-press drag feedback
- dnd-kit's TouchSensor has a 200ms activation delay; previously
the row gave no cue during the hold window so users lifted
early thinking the schedule didn't register.
- Fix: added `isPressing` state toggled via capture-phase
onPointerDown/Up/Cancel handlers on the row tr, gated on
`pointerType === "touch"` && `effectiveDragEnabled`. Capture
phase chosen so handlers don't collide with safeRowDragListeners
(which spreads dnd-kit's bubble-phase onPointerDown).
- Visual cue: subtle `inset 0 0 0 2px hexToRgba(highlightColor,
0.45)` ring composed AFTER quickOpen/selection/current rings so
it never outranks a real selection state. Two defensive
useEffects clear `isPressing`: one when drag becomes disabled
(capability flip / edit-mode toggle), one tied to the
`isDragging` lifecycle so dnd-kit-managed release paths (drop
outside the row, sensor-managed cancel) can't leave a stale
ring behind.
- data-pressing="true" attribute exposed on tr for tests.
- TouchSensor delay (200ms) and tolerance (8px) unchanged.
Tests added:
- executive-meetings-quick-actions-dialog-layout.spec.mjs: asserts
close button + title bounding boxes don't overlap in AR + EN.
- executive-meetings-row-touch-press-feedback.spec.mjs: uses
hasTouch+isMobile + CDP Input.dispatchTouchEvent, asserts
data-pressing flips true on touchstart, clears on touchend, and
tap-to-open quick-actions dialog still works for short taps;
second test verifies the cue persists past the 200ms window.
Verification: pnpm tsc --noEmit clean. Pre-existing `test` workflow
failures (rotate-content/font-settings/notes-share) are unrelated to
this change.
Two scoped UX fixes on Executive Meetings schedule, both confined to
artifacts/tx-os/src/pages/executive-meetings.tsx (shared
src/components/ui/dialog.tsx untouched per task spec).
1) Quick Actions dialog header overlap
- Radix DialogContent renders the Close (X) at physical
`right-4 top-4` regardless of `dir`. In AR (RTL) the
`text-right`-aligned title "إجراءات سريعة" pinned to the same
edge and visibly overlapped the X.
- Fix: added `pr-8` to the per-row Quick Actions DialogHeader so
the title's right edge always clears the close button. Works
for AR (overlap eliminated) and EN (just adds trailing slack).
2) iPad long-press drag feedback
- dnd-kit's TouchSensor has a 200ms activation delay; previously
the row gave no cue during the hold window so users lifted
early thinking the schedule didn't register.
- Fix: added `isPressing` state toggled via capture-phase
onPointerDown/Up/Cancel handlers on the row tr, gated on
`pointerType === "touch"` && `effectiveDragEnabled`. Capture
phase chosen so handlers don't collide with safeRowDragListeners
(which spreads dnd-kit's bubble-phase onPointerDown).
- Visual cue: subtle `inset 0 0 0 2px hexToRgba(highlightColor,
0.45)` ring composed AFTER quickOpen/selection/current rings so
it never outranks a real selection state. Defensive useEffect
clears `isPressing` if drag becomes disabled mid-press.
- data-pressing="true" attribute exposed on tr for tests.
- TouchSensor delay (200ms) and tolerance (8px) unchanged.
Tests added:
- executive-meetings-quick-actions-dialog-layout.spec.mjs: asserts
close button + title bounding boxes don't overlap in AR + EN.
- executive-meetings-row-touch-press-feedback.spec.mjs: uses
hasTouch+isMobile + CDP Input.dispatchTouchEvent, asserts
data-pressing flips true on touchstart, clears on touchend, and
tap-to-open quick-actions dialog still works for short taps;
second test verifies the cue persists past the 200ms window.
Verification: pnpm tsc --noEmit clean. Pre-existing `test` workflow
failures (rotate-content/font-settings/notes-share) are unrelated to
this change.
Lifted the per-row missing-time drag block (originally #492). Meetings
without a (startTime, endTime) tuple are a normal state and now rotate
freely alongside timed rows.
Server (artifacts/api-server/src/routes/executive-meetings.ts):
- Dropped the `no_time_window` early-return guard in
/api/executive-meetings/rotate-content.
- Sort + slot construction now tolerate null start times (NULLS LAST,
tie-break by id) so a null tuple rotates as a real slot.
- All other safeguards (cancelled_in_rotate, optimistic lock,
renumberDayByStartTime, audit logging) remain intact.
Client (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Removed missingTimeCount / dayRotatable memos, the dayRotatable
prop wiring, dragBlockedByMissingTime + effectiveDragEnabled
composition, the row's data-drag-blocked / native title /
cursor-not-allowed-opacity-80 / aria-disabled overrides, the entire
DragBlockedTooltipButton component + its render site, and the
no_time_window errorToast branch in rotateContent. Pruned now-dead
Tooltip + AlertTriangle imports.
i18n: removed the `executiveMeetings.rotate.needsTimeWindow` block
(tooltip + errorToast) from en.json and ar.json.
Tests: deleted `executive-meetings-rotate-needs-time-window.spec.mjs`
and added `executive-meetings-rotate-allows-missing-times.spec.mjs`,
which inserts 3 meetings (one with null times), verifies no
drag-blocked warning button / aria-disabled, drags the top row to the
bottom slot, asserts the rotate-content POST succeeds, and confirms
exactly one row still has a null tuple after rotation.
Verified: row-drag, row-quick-actions, and the new spec all pass
(8/8). Architect code review PASSED.
Lifted the per-row missing-time drag block (originally #492). Meetings
without a (startTime, endTime) tuple are a normal state and now rotate
freely alongside timed rows.
Server (artifacts/api-server/src/routes/executive-meetings.ts):
- Dropped the `no_time_window` early-return guard in
/api/executive-meetings/rotate-content.
- Sort + slot construction now tolerate null start times (NULLS LAST,
tie-break by id) so a null tuple rotates as a real slot.
- All other safeguards (cancelled_in_rotate, optimistic lock,
renumberDayByStartTime, audit logging) remain intact.
Client (artifacts/tx-os/src/pages/executive-meetings.tsx):
- Removed missingTimeCount / dayRotatable memos, the dayRotatable
prop wiring, dragBlockedByMissingTime + effectiveDragEnabled
composition, the row's data-drag-blocked / native title /
cursor-not-allowed-opacity-80 / aria-disabled overrides, the entire
DragBlockedTooltipButton component + its render site, and the
no_time_window errorToast branch in rotateContent. Pruned now-dead
Tooltip + AlertTriangle imports.
i18n: removed the `executiveMeetings.rotate.needsTimeWindow` block
(tooltip + errorToast) from en.json and ar.json.
Tests: deleted `executive-meetings-rotate-needs-time-window.spec.mjs`
and added `executive-meetings-rotate-allows-missing-times.spec.mjs`,
which inserts 3 meetings (one with null times), verifies no
drag-blocked warning button / aria-disabled, drags the top row to the
bottom slot, asserts the rotate-content POST succeeds, and confirms
exactly one row still has a null tuple after rotation.
Verified: row-drag, row-quick-actions, and the new spec all pass
(8/8). Architect code review PASSED.
- ScheduleSection: gate `quickActionsCanMutate` on `canMutate && !editMode`
so clicking a row in edit mode no longer opens the Postpone dialog.
In view mode the quick-actions surface still works as before.
- MeetingRow: add a useEffect that force-closes local `quickOpen` when
capability flips off (edit-mode toggle while dialog is open) and gate
`quickOpenShadow` + `data-quick-open` on capability so no stale frame
paints. Caught by code review.
- Remove the amber `em-rotate-blocked-hint` missing-time banner per
product feedback. Kept `missingTimeCount` / `dayRotatable` so row
drag still aborts client-side and the per-row tooltip + native
title still explain why.
- Drop now-unused i18n keys `executiveMeetings.rotate.needsTimeWindow.hint*`
from en.json and ar.json (zero/one/two/few/many/other variants).
`tooltip` + `errorToast` strings stay (consumed by tooltip button
+ rotate-content catch handler).
- Update executive-meetings-rotate-needs-time-window.spec.mjs to drop
the banner visibility assertion and the post-unblock `toHaveCount(0)`
re-check. Tooltip / aria-disabled / no-rotate-POST / successful-
rotate-after-unblock checks remain.
Verified: rotate-needs-time-window + row-quick-actions specs (7 tests)
all pass with --workers=1.
The /api/executive-meetings/rotate-content endpoint hard-rejects with
`code: "no_time_window"` whenever any visible non-cancelled meeting on
the date is missing (start_time, end_time). Before this change, dragging
a row on such a day produced an opaque English error toast that
confused AR users and didn't tell them what to fix.
UI changes:
- Compute `missingTimeCount` / `dayRotatable` from `orderedMeetings`
in executive-meetings.tsx and thread `dayRotatable` into MeetingRow.
- MeetingRow gates useSortable + safeRowDragListeners on
`effectiveDragEnabled = dragEnabled && dayRotatable`. When drag is
blocked specifically by missing time, the <tr> shows
`cursor-not-allowed`, dimmed opacity, `aria-disabled="true"`,
`data-drag-blocked="no_time_window"`, and a bilingual native `title`
(desktop hover fallback). The aria-disabled is spread AFTER dnd-kit's
attributes so it wins the prop merge.
- New `DragBlockedTooltipButton` rendered inside each blocked row's #
cell — a Radix Tooltip-wrapped warning icon that opens on hover,
focus, AND tap (manual open toggle) so iPad users can read the
explanation (native title long-press is unreliable on touch).
- Inline amber `em-rotate-blocked-hint` banner above the bulk toolbar
surfaces the missing-time count using i18next's CLDR plural resolver
(`t(key, { count })`) so AR picks the correct
zero/one/two/few/many/other form and EN picks one/other.
- rotateContent's catch handler detects ApiError code "no_time_window"
(local apiJson now attaches .code/.status to thrown Errors) and
shows the bilingual `executiveMeetings.rotate.needsTimeWindow.errorToast`
instead of the raw server message.
- Locale keys added under `executiveMeetings.rotate.needsTimeWindow`
in en.json + ar.json (tooltip, hint plurals, errorToast). AR has
full zero/one/two/few/many/other variants.
- ScheduleSection's `t` prop type widened to accept i18next options.
Tests:
- New tests/executive-meetings-rotate-needs-time-window.spec.mjs:
inserts one untimed + two timed meetings, asserts the hint banner +
every row's aria-disabled + data-drag-blocked, asserts the
Tooltip-wrapped info button is visible with the correct localized
aria-label + tooltip body (substring match — Radix renders sr-only
duplicate), attempts a drag and asserts NO rotate-content POST is
sent and DB state is unchanged. Then UPDATE-s the missing time and
asserts: hint disappears, aria-disabled lifts on every row, info
button is gone, drag now succeeds (rotate-content POST + DB
start_time mutation).
- Existing executive-meetings-row-drag, touch-reorder, and
row-quick-actions specs still pass.
Files: artifacts/tx-os/src/pages/executive-meetings.tsx,
artifacts/tx-os/src/locales/{en,ar}.json,
artifacts/tx-os/tests/executive-meetings-rotate-needs-time-window.spec.mjs
Server route untouched.
The /api/executive-meetings/rotate-content endpoint hard-rejects with
`code: "no_time_window"` whenever any visible non-cancelled meeting on
the date is missing (start_time, end_time). Before this change, dragging
a row on such a day produced an opaque English error toast ("Every
meeting must have a scheduled time window to rotate") that confused AR
users and didn't tell them what to fix.
Fix:
- Compute `missingTimeCount` / `dayRotatable` from `orderedMeetings`
in executive-meetings.tsx and thread `dayRotatable` into MeetingRow.
- MeetingRow now gates useSortable + safeRowDragListeners on
`effectiveDragEnabled = dragEnabled && dayRotatable`. When drag is
blocked specifically by missing time, the <tr> shows
`cursor-not-allowed`, dimmed opacity, `aria-disabled="true"`,
`data-drag-blocked="no_time_window"`, and a bilingual `title`
tooltip. The aria-disabled is spread AFTER dnd-kit's attributes so
it wins the prop merge.
- Inline amber `em-rotate-blocked-hint` banner above the bulk toolbar
surfaces the missing-time count so mutators see why drag is paused.
- rotateContent's catch handler now detects ApiError code
"no_time_window" (local apiJson now attaches .code/.status to the
thrown Error) and shows the bilingual
`executiveMeetings.rotate.needsTimeWindow.errorToast` instead of
the raw server message.
- Locale keys added under `executiveMeetings.rotate.needsTimeWindow`
in en.json + ar.json (tooltip, hint_one/_other [+ AR plurals],
errorToast).
Tests:
- New tests/executive-meetings-rotate-needs-time-window.spec.mjs
inserts one untimed + two timed meetings and asserts the hint
banner + every row's aria-disabled + data-drag-blocked, then
attempts a drag and asserts NO rotate-content POST is sent and
DB state is unchanged. After UPDATE-ing the missing time, asserts
the hint and aria-disabled lift.
- Existing executive-meetings-row-drag and touch-reorder specs still
pass — drag works exactly as before on fully-timed days.
Files: artifacts/tx-os/src/pages/executive-meetings.tsx,
artifacts/tx-os/src/locales/{en,ar}.json,
artifacts/tx-os/tests/executive-meetings-rotate-needs-time-window.spec.mjs
Server route untouched.
The user reported that the small quick-actions popover (which holds
the Postpone trigger) was hard to find — it floated next to the
clicked row and they sometimes lost track of which meeting they
were about to act on. They asked for the surface to appear in the
center of the screen, and for the row they clicked to be clearly
framed in a color while the surface is open.
Changes:
- Replaced the row's row-anchored Popover (PopoverAnchor +
PopoverContent) with a centered Dialog (DialogContent) that
Radix renders into a portal. The Dialog uses the existing
`executiveMeetings.quickActions.label` translation key for its
title (already present in both Arabic and English) and reuses
#490's polished Postpone Button unchanged. Test ID stays
`em-row-quick-${id}` so existing e2e specs continue to target
the same surface.
- Added a third inset-shadow ring (`inset 0 0 0 3px
${highlightColor}`) to the row's composed boxShadow stack while
the row's quickOpen state is true. Listed first in the stack so
it becomes the outermost frame and wins visually over the
existing current-meeting / bulk-select rings. Uses the
page-level highlightPrefs.color so the frame matches the user's
chosen accent. Also exposes `data-quick-open="true"` on the
<tr> for tests.
- Updated the row-quick-actions e2e: the postpone-end-to-end test
now asserts `role="dialog"` on the surface and the row carries
`data-quick-open` while open and loses it after Postpone is
clicked. Added a separate test for Escape closing the surface
and clearing the row frame.
Verified: 5/5 quick-actions tests pass, plus both drag specs
(row-drag + iPad touch-reorder) still pass.
The user reported that the small quick-actions popover (which holds
the Postpone trigger) was hard to find — it floated next to the
clicked row and they sometimes lost track of which meeting they
were about to act on. They asked for the surface to appear in the
center of the screen, and for the row they clicked to be clearly
framed in a color while the surface is open.
Changes:
- Replaced the row's row-anchored Popover (PopoverAnchor +
PopoverContent) with a centered Dialog (DialogContent) that
Radix renders into a portal. The Dialog uses the existing
`executiveMeetings.quickActions.label` translation key for its
title (already present in both Arabic and English) and reuses
#490's polished Postpone Button unchanged. Test ID stays
`em-row-quick-${id}` so existing e2e specs continue to target
the same surface.
- Added a third inset-shadow ring (`inset 0 0 0 3px
${highlightColor}`) to the row's composed boxShadow stack while
the row's quickOpen state is true. Listed first in the stack so
it becomes the outermost frame and wins visually over the
existing current-meeting / bulk-select rings. Uses the
page-level highlightPrefs.color so the frame matches the user's
chosen accent. Also exposes `data-quick-open="true"` on the
<tr> for tests.
- Updated the row-quick-actions e2e: the postpone-end-to-end test
now asserts `role="dialog"` on the surface and the row carries
`data-quick-open` while open and loses it after Postpone is
clicked. Added a separate test for Escape closing the surface
and clearing the row frame.
Verified: 5/5 quick-actions tests pass, plus both drag specs
(row-drag + iPad touch-reorder) still pass.
The user reported that the small quick-actions popover (which holds
the Postpone trigger) was hard to find — it floated next to the
clicked row and they sometimes lost track of which meeting they
were about to act on. They asked for the surface to appear in the
center of the screen, and for the row they clicked to be clearly
framed in a color while the surface is open.
Changes:
- Replaced the row's row-anchored Popover (PopoverAnchor +
PopoverContent) with a centered Dialog (DialogContent) that
Radix renders into a portal. The Dialog uses the existing
`executiveMeetings.quickActions.label` translation key for its
title (already present in both Arabic and English) and reuses
#490's polished Postpone Button unchanged. Test ID stays
`em-row-quick-${id}` so existing e2e specs continue to target
the same surface.
- Added a third inset-shadow ring (`inset 0 0 0 3px
${highlightColor}`) to the row's composed boxShadow stack while
the row's quickOpen state is true. Listed first in the stack so
it becomes the outermost frame and wins visually over the
existing current-meeting / bulk-select rings. Uses the
page-level highlightPrefs.color so the frame matches the user's
chosen accent. Also exposes `data-quick-open="true"` on the
<tr> for tests.
- Updated the row-quick-actions e2e: the postpone-end-to-end test
now asserts `role="dialog"` on the surface and the row carries
`data-quick-open` while open and loses it after Postpone is
clicked. Added a separate test for Escape closing the surface
and clearing the row frame.
Verified: 5/5 quick-actions tests pass, plus both drag specs
(row-drag + iPad touch-reorder) still pass.