| ... | @@ -242,6 +242,82 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro | ... | @@ -242,6 +242,82 @@ export function ComposerBannerStack({ className, items }: ComposerBannerStackPro |
| 242 | ); | 242 | ); |
| 243 | } | 243 | } |
| 244 | | 244 | |
| | 245 | /** Keep full descriptions reachable only when their inline copy is clipped. */ |
| | 246 | function NoticeDescription({ children, compact }: { children: ReactNode; compact?: boolean }) { |
| | 247 | const descriptionRef = useRef<HTMLSpanElement>(null); |
| | 248 | const detailsRef = useRef<HTMLButtonElement>(null); |
| | 249 | const [showDetails, setShowDetails] = useState(false); |
| | 250 | |
| | 251 | useLayoutEffect(() => { |
| | 252 | const description = descriptionRef.current; |
| | 253 | if (!description) return; |
| | 254 | const measure = () => { |
| | 255 | // Ignore the space taken by the details button itself so it cannot |
| | 256 | // sustain its own overflow after the description would otherwise fit. |
| | 257 | const recoveredWidth = detailsRef.current ? detailsRef.current.offsetWidth + 4 : 0; |
| | 258 | const hidden = getComputedStyle(description).position === "absolute"; |
| | 259 | setShowDetails( |
| | 260 | hidden || |
| | 261 | [description, ...description.querySelectorAll("*")].some( |
| | 262 | (element) => element.scrollWidth > element.clientWidth + recoveredWidth, |
| | 263 | ), |
| | 264 | ); |
| | 265 | }; |
| | 266 | measure(); |
| | 267 | const observer = new ResizeObserver(measure); |
| | 268 | observer.observe(description); |
| | 269 | // A child can reveal new text without resizing its clipped box. |
| | 270 | const mutations = new MutationObserver(measure); |
| | 271 | mutations.observe(description, { childList: true, subtree: true, characterData: true }); |
| | 272 | return () => { |
| | 273 | observer.disconnect(); |
| | 274 | mutations.disconnect(); |
| | 275 | }; |
| | 276 | }, []); |
| | 277 | |
| | 278 | return ( |
| | 279 | <span className={compact ? "contents" : "flex min-w-8 flex-1 items-center gap-1"}> |
| | 280 | <span |
| | 281 | ref={descriptionRef} |
| | 282 | className={cn( |
| | 283 | "min-w-0 truncate text-muted-foreground", |
| | 284 | compact && "shrink-[9999] @max-[400px]:sr-only", |
| | 285 | )} |
| | 286 | > |
| | 287 | {children} |
| | 288 | </span> |
| | 289 | {showDetails ? ( |
| | 290 | <Popover> |
| | 291 | <PopoverTrigger |
| | 292 | openOnHover |
| | 293 | render={ |
| | 294 | <Button |
| | 295 | ref={detailsRef} |
| | 296 | size="icon-xs" |
| | 297 | variant="ghost-muted" |
| | 298 | aria-label="Show notice details" |
| | 299 | className="flex-none" |
| | 300 | /> |
| | 301 | } |
| | 302 | > |
| | 303 | <InfoIcon /> |
| | 304 | </PopoverTrigger> |
| | 305 | <PopoverPopup |
| | 306 | aria-label="Notice details" |
| | 307 | tooltipStyle |
| | 308 | side="top" |
| | 309 | className="max-w-80 whitespace-normal text-pretty wrap-anywhere" |
| | 310 | > |
| | 311 | <ComposerBanner.Scroll className="max-h-[min(var(--available-height),24rem,40dvh)]"> |
| | 312 | {children} |
| | 313 | </ComposerBanner.Scroll> |
| | 314 | </PopoverPopup> |
| | 315 | </Popover> |
| | 316 | ) : null} |
| | 317 | </span> |
| | 318 | ); |
| | 319 | } |
| | 320 | |
| 245 | function ComposerBannerStackAlert({ | 321 | function ComposerBannerStackAlert({ |
| 246 | item, | 322 | item, |
| 247 | attached, | 323 | attached, |
| ... | @@ -278,44 +354,9 @@ function ComposerBannerStackAlert({ | ... | @@ -278,44 +354,9 @@ function ComposerBannerStackAlert({ |
| 278 | <ComposerBanner.Content className="whitespace-nowrap"> | 354 | <ComposerBanner.Content className="whitespace-nowrap"> |
| 279 | <span className="min-w-0 truncate font-medium leading-7 sm:leading-6">{item.title}</span> | 355 | <span className="min-w-0 truncate font-medium leading-7 sm:leading-6">{item.title}</span> |
| 280 | {item.description ? ( | 356 | {item.description ? ( |
| 281 | <span className={item.compact ? "contents" : "flex min-w-8 flex-1 items-center gap-1"}> | 357 | <NoticeDescription compact={item.compact ?? false}> |
| 282 | <span | 358 | {item.description} |
| 283 | className={cn( | 359 | </NoticeDescription> |
| 284 | "min-w-0 truncate text-muted-foreground", | | |
| 285 | item.compact && "shrink-[9999] @max-[400px]:sr-only", | | |
| 286 | )} | | |
| 287 | > | | |
| 288 | {item.description} | | |
| 289 | </span> | | |
| 290 | <Popover> | | |
| 291 | <PopoverTrigger | | |
| 292 | openOnHover | | |
| 293 | render={ | | |
| 294 | <Button | | |
| 295 | size="icon-xs" | | |
| 296 | variant="ghost" | | |
| 297 | aria-label="Show notice details" | | |
| 298 | className={cn( | | |
| 299 | "flex-none text-muted-foreground hover:text-foreground", | | |
| 300 | item.compact && "hidden @max-[400px]:inline-flex", | | |
| 301 | )} | | |
| 302 | /> | | |
| 303 | } | | |
| 304 | > | | |
| 305 | <InfoIcon className="size-3.5" /> | | |
| 306 | </PopoverTrigger> | | |
| 307 | <PopoverPopup | | |
| 308 | aria-label="Notice details" | | |
| 309 | tooltipStyle | | |
| 310 | side="top" | | |
| 311 | className="max-w-80 whitespace-normal text-pretty wrap-anywhere" | | |
| 312 | > | | |
| 313 | <ComposerBanner.Scroll className="max-h-[min(var(--available-height),24rem,40dvh)]"> | | |
| 314 | {item.description} | | |
| 315 | </ComposerBanner.Scroll> | | |
| 316 | </PopoverPopup> | | |
| 317 | </Popover> | | |
| 318 | </span> | | |
| 319 | ) : null} | 360 | ) : null} |
| 320 | </ComposerBanner.Content> | 361 | </ComposerBanner.Content> |
| 321 | {item.actions || item.onDismiss ? ( | 362 | {item.actions || item.onDismiss ? ( |