tx-os(#344): polish UpcomingMeetingAlert per user feedback

Four UI changes requested by an Arabic-speaking user on the floating
"اجتماع يبدأ قريباً" popup:

1. Time window LTR — wrap the header `alert-time-window` span in
   `dir="ltr"` so the start time always appears on the left and the
   end on the right, even inside the Arabic RTL UI. Prevents the
   "م 3:20 – م 3:10" reversal seen in the user's screenshot.
2. Drop the duplicate "Time" row inside DetailsPanel — the same
   start/end window is already shown in the header strip. Removed
   the `if (timeWindow) rows.push({ key: "time", ... })` block; the
   `timeWindow` local was deleted along with it. Header is now the
   single source of truth.
3. Remove the labelled "تجاهل التنبيه" footer button. The X close
   button in the header still calls `handleDismiss`, so users can
   still dismiss; the action bar is now just Done / Postpone /
   Details. `handleDismiss`, `dismissToast`, and the i18n keys are
   left intact for the X button.
4. Make the "يبدأ خلال N دقائق" / "يبدأ الآن" countdown blink red.
   Added a dedicated `@keyframes em-blink` (1s, opacity 1 → 0.15 →
   1) and `.em-blink` class in `src/index.css`, with a
   `prefers-reduced-motion` override that pins it at full opacity.
   Countdown span uses `text-red-600 em-blink font-semibold`. The
   previously-used `accentText` local had no remaining consumers,
   so it was removed (replaced with an explanatory comment).

Files:
- artifacts/tx-os/src/components/executive-meetings/upcoming-meeting-alert.tsx
- artifacts/tx-os/src/index.css

Verification: `pnpm -C artifacts/tx-os exec tsc --noEmit` clean.
Architect review APPROVED (PASS). No tests referenced the removed
`alert-dismiss` / `alert-details-time` testids.

Pre-existing failures (unrelated): api-server workflow and the
combined `test` workflow continue to fail with the same upstream
issues observed in #342/#343 trajectory; this change does not
touch the API server or e2e test infrastructure.
This commit is contained in:
riyadhafraa
2026-05-03 12:26:38 +00:00
parent b2e6512d0c
commit ff2304c4c2
2 changed files with 38 additions and 29 deletions
@@ -481,7 +481,8 @@ export function UpcomingMeetingAlert() {
const dividerColor = hexToRgba(alertPrefs.accent, 0.45);
const closeHoverBg = hexToRgba(alertPrefs.accent, 0.25);
const detailsBg = hexToRgba(alertPrefs.accent, 0.12);
const accentText = alertPrefs.accent;
// #344: `accentText` was removed because the only consumer (the
// "starts in N minutes" headline) now uses a fixed red blink class.
return (
<>
@@ -561,8 +562,7 @@ export function UpcomingMeetingAlert() {
</div>
<div className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
<span
className="font-semibold"
style={{ color: accentText }}
className="em-blink font-semibold text-red-600"
data-testid="alert-countdown"
>
{headline}
@@ -571,6 +571,12 @@ export function UpcomingMeetingAlert() {
<span
className="inline-flex items-center gap-1 opacity-80"
data-testid="alert-time-window"
// The time window is always rendered LTR (start on the
// left, end on the right) even inside the Arabic UI —
// requested explicitly by the user (#344). The clock
// icon precedes the digits in source order; with
// dir="ltr" that puts it visually on the left.
dir="ltr"
>
<Clock className="h-3 w-3" aria-hidden="true" />
{meeting.endTime
@@ -623,16 +629,13 @@ export function UpcomingMeetingAlert() {
{t("executiveMeetings.alert.postpone")}
</Button>
) : null}
<Button
type="button"
size="sm"
variant="outline"
onClick={handleDismiss}
disabled={savingAction !== null}
data-testid="alert-dismiss"
>
{t("executiveMeetings.alert.dismiss")}
</Button>
{/*
#344: The labelled "Dismiss alert" button was removed at
the user's request — the X close button in the header
(which still calls handleDismiss) is the single way to
hide the popup, keeping the action bar focused on
"Done" / "Postpone" / "Details".
*/}
<Button
type="button"
size="sm"
@@ -1771,13 +1774,10 @@ function DetailsPanel({
}
})();
// #307: Pre-compute the rendered time-window string so the row is
// skipped entirely when the meeting has no scheduled times.
const timeWindow = meeting.startTime
? meeting.endTime
? `${formatTime(meeting.startTime, lang)} ${formatTime(meeting.endTime, lang)}`
: formatTime(meeting.startTime, lang)
: null;
// #344: The "time" row in the details panel duplicated the time
// window already shown in the header strip, so it was removed at
// the user's request. Header is the single source of truth for the
// start/end times.
// Each row renders a label cell + a value cell inside the parent
// CSS grid. Skipping rows whose value is missing keeps the table
@@ -1791,15 +1791,6 @@ function DetailsPanel({
};
const rows: Row[] = [];
if (timeWindow) {
rows.push({
key: "time",
icon: <Clock className="h-3.5 w-3.5 opacity-70" aria-hidden="true" />,
label: t("executiveMeetings.alert.detailsTime"),
value: <span data-testid="alert-details-time">{timeWindow}</span>,
});
}
if (meeting.location && meeting.location.trim()) {
rows.push({
key: "location",
+18
View File
@@ -339,3 +339,21 @@
}
}
/*
* #344: Attention-grabbing blink for the "starts in N minutes" headline
* inside the floating UpcomingMeetingAlert. Tailwind's `animate-pulse`
* only fades to opacity 0.5, which doesn't read as a true blink, so we
* ship a dedicated keyframe that drops to 0.15 and back. Respects the
* user's reduced-motion preference by stopping at full opacity.
*/
@keyframes em-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.15; }
}
.em-blink {
animation: em-blink 1s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.em-blink { animation: none; opacity: 1; }
}