diff --git a/client/src/components/SystemNotices/SystemNoticeModal.tsx b/client/src/components/SystemNotices/SystemNoticeModal.tsx index 837bef10..82ae2189 100644 --- a/client/src/components/SystemNotices/SystemNoticeModal.tsx +++ b/client/src/components/SystemNotices/SystemNoticeModal.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useLayoutEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { flushSync } from 'react-dom'; import { useNavigate } from 'react-router-dom'; import { Info, AlertTriangle, AlertOctagon, X, ChevronLeft, ChevronRight } from 'lucide-react'; @@ -71,159 +71,168 @@ function NoticeContent({ notice, title, body, ctaLabel, titleId, bodyId, isDark, : DefaultIcon; return ( -
+
{/* Dismiss X button — only on last page so users read all notices */} {notice.dismissible && isLastPage && ( )} - {/* Hero image (not inline) */} - {notice.media && notice.media.placement !== 'inline' && ( -
- {t(notice.media.altKey)} { (e.target as HTMLImageElement).style.display = 'none'; }} - /> -
- )} - - {/* Special warm header for Heart icon (thank-you notice) */} - {notice.icon === 'Heart' && !notice.media && ( -
-
-
-
- -
-
-

{title}

-

TREK 3.0

-
-
-
- )} - -
- {/* Severity icon (when no hero and not Heart) */} - {!notice.media && notice.icon !== 'Heart' && ( -
- -
- )} - - {/* Title (not for Heart — rendered in gradient header) */} - {(notice.icon !== 'Heart' || notice.media) && ( -

- {title} -

- )} - - {/* Body — markdown (long body text uses left-aligned layout) */} -
- {body}

}> - ( - - {children} - - ), - p: ({ children }) => { - // Signature line styling (e.g. "— Maurice") - const text = typeof children === 'string' ? children : Array.isArray(children) ? children.find(c => typeof c === 'string') : ''; - if (typeof text === 'string' && text.trim().startsWith('—') && text.trim().length < 30) { - return

{children}

; - } - return

{children}

; - }, - hr: () => ( -
-
- -
-
- ), - strong: ({ children }) => {children}, - ul: ({ children }) =>
    {children}
, - ol: ({ children }) =>
    {children}
, - }} - > - {body} - - -
- - {/* Inline image */} - {notice.media?.placement === 'inline' && ( + {/* Scrollable content — vertically centered when shorter than available space */} +
+ {/* Hero image (not inline) */} + {notice.media && notice.media.placement !== 'inline' && (
{t(notice.media.altKey)} { (e.target as HTMLImageElement).style.display = 'none'; }} />
)} - {/* Highlights */} - {notice.highlights && notice.highlights.length > 0 && ( -
    - {notice.highlights.map((h, i) => { - const HIcon: React.ElementType | null = h.iconName - ? ((LucideIcons as Record)[h.iconName] as React.ElementType) ?? null - : null; - return ( -
  • - {HIcon - ? - : - } - {t(h.labelKey)} -
  • - ); - })} -
+ {/* Special warm header for Heart icon (thank-you notice) */} + {notice.icon === 'Heart' && !notice.media && ( +
+
+
+
+ +
+
+

{title}

+

TREK 3.0

+
+
+
)} +
+ {/* Severity icon (when no hero and not Heart) */} + {!notice.media && notice.icon !== 'Heart' && ( +
+ +
+ )} + + {/* Title (not for Heart — rendered in gradient header) */} + {(notice.icon !== 'Heart' || notice.media) && ( +

+ {title} +

+ )} + + {/* Body — markdown */} +
+ {body}

}> + ( + + {children} + + ), + p: ({ children }) => { + // Signature line styling (e.g. "— Maurice") + const text = typeof children === 'string' ? children : Array.isArray(children) ? children.find(c => typeof c === 'string') : ''; + if (typeof text === 'string' && text.trim().startsWith('—') && text.trim().length < 30) { + return

{children}

; + } + return

{children}

; + }, + hr: () => ( +
+
+ +
+
+ ), + strong: ({ children }) => {children}, + ul: ({ children }) =>
    {children}
, + ol: ({ children }) =>
    {children}
, + }} + > + {body} + + +
+ + {/* Inline image */} + {notice.media?.placement === 'inline' && ( +
+ {t(notice.media.altKey)} { (e.target as HTMLImageElement).style.display = 'none'; }} + /> +
+ )} + + {/* Highlights */} + {notice.highlights && notice.highlights.length > 0 && ( +
    + {notice.highlights.map((h, i) => { + const HIcon: React.ElementType | null = h.iconName + ? ((LucideIcons as Record)[h.iconName] as React.ElementType) ?? null + : null; + return ( +
  • + {HIcon + ? + : + } + {t(h.labelKey)} +
  • + ); + })} +
+ )} +
+
+ + {/* Sticky footer — pager + CTA, always anchored at the bottom of the slot */} +
{/* Pager — dots, arrows, counter (only when multiple notices) */} {total > 1 && ( -
+
@@ -247,7 +256,7 @@ function NoticeContent({ notice, title, body, ctaLabel, titleId, bodyId, isDark, onClick={onNext} disabled={!canPage || currentPage === total - 1} aria-label={t('system_notice.pager.next')} - className="p-1 rounded text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 disabled:opacity-30 disabled:cursor-not-allowed transition-colors" + className="px-2 py-1 rounded border border-slate-200 dark:border-slate-700 text-slate-500 hover:text-slate-700 dark:hover:text-slate-200 hover:bg-slate-100 dark:hover:bg-slate-800 disabled:opacity-30 disabled:cursor-not-allowed transition-colors" > @@ -262,17 +271,8 @@ function NoticeContent({ notice, title, body, ctaLabel, titleId, bodyId, isDark, )} {/* CTA + dismiss link */} -
- {!isLastPage && total > 1 ? ( - /* Non-last page: "Next" button to advance through all notices */ - - ) : ctaLabel ? ( +
+ {ctaLabel && isLastPage ? ( - ) : ( + ) : (notice.dismissible || isLastPage) && (