Merge pull request 'Feat/messaging ui foundation' (#8) from feat/messaging-ui-foundation into dev

Reviewed-on: #8
This commit was merged in pull request #8.
This commit is contained in:
2026-07-22 21:42:56 +00:00
6 changed files with 101 additions and 30 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@insignia/iios-messaging-ui",
"version": "0.1.2",
"version": "0.1.4",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
@@ -1,5 +1,6 @@
import { useState } from 'react';
import { useRef, useState } from 'react';
import { highlightMentions } from '../mentions';
import { PopoverPortal } from './popover-portal';
import type { Attachment } from '../types';
import type { UiMessage } from '../hooks/use-messages';
@@ -43,6 +44,7 @@ export function MessageItem({
onOpenThread?: (messageId: string) => void;
}) {
const [pickerOpen, setPickerOpen] = useState(false);
const reactBtnRef = useRef<HTMLButtonElement>(null);
const m = message;
return (
@@ -55,10 +57,11 @@ export function MessageItem({
<div className="miu-msg-actions">
{canReact ? (
<div className="miu-react-wrap">
<button type="button" className="miu-react-btn" title="React" onClick={() => setPickerOpen((p) => !p)}>
<button ref={reactBtnRef} type="button" className="miu-react-btn" title="React" onClick={() => setPickerOpen((p) => !p)}>
🙂
</button>
{pickerOpen ? (
<PopoverPortal anchorRef={reactBtnRef} onClose={() => setPickerOpen(false)}>
<div className="miu-react-picker">
{REACTION_EMOJIS.map((e) => (
<button
@@ -74,6 +77,7 @@ export function MessageItem({
</button>
))}
</div>
</PopoverPortal>
) : null}
</div>
) : null}
@@ -8,7 +8,7 @@ const THEME_VARS = [
'--miu-text', '--miu-muted', '--miu-accent', '--miu-accent-text', '--miu-radius',
] as const;
function copyThemeVars(): Record<string, string> {
export function copyThemeVars(): Record<string, string> {
if (typeof document === 'undefined') return {};
const src = document.querySelector('.miu-messenger, .miu-inbox');
if (!src) return {};
@@ -0,0 +1,67 @@
import { useEffect, useLayoutEffect, useState, type CSSProperties, type ReactNode, type RefObject } from 'react';
import { createPortal } from 'react-dom';
import { copyThemeVars } from './modal-portal';
/**
* A small popover (e.g. the reaction picker) rendered into document.body so it is never clipped by
* a scroll container's `overflow: hidden` — the reported "emoji picker goes beneath the container"
* bug. Positioned fixed just below the anchor, right-aligned to it, and re-placed on scroll/resize.
* Closes on outside pointer-down, scroll of a different element, or Escape. Carries the SDK theme
* tokens (copied from the live surface) since a body-portaled node is outside the themed subtree.
*/
export function PopoverPortal({
anchorRef,
onClose,
children,
}: {
anchorRef: RefObject<HTMLElement | null>;
onClose: () => void;
children: ReactNode;
}) {
const [pos, setPos] = useState<{ top: number; left: number } | null>(null);
const [vars] = useState(copyThemeVars);
useLayoutEffect(() => {
const el = anchorRef.current;
if (!el) return;
const place = (): void => {
const r = el.getBoundingClientRect();
setPos({ top: r.bottom + 4, left: r.right });
};
place();
window.addEventListener('resize', place);
window.addEventListener('scroll', place, true);
return () => {
window.removeEventListener('resize', place);
window.removeEventListener('scroll', place, true);
};
}, [anchorRef]);
useEffect(() => {
const onDown = (e: PointerEvent): void => {
const target = e.target as Node;
if (anchorRef.current?.contains(target)) return;
if ((target as Element).closest?.('.miu-popover')) return;
onClose();
};
const onKey = (e: KeyboardEvent): void => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('pointerdown', onDown, true);
document.addEventListener('keydown', onKey);
return () => {
document.removeEventListener('pointerdown', onDown, true);
document.removeEventListener('keydown', onKey);
};
}, [anchorRef, onClose]);
if (typeof document === 'undefined' || !pos) return null;
return createPortal(
<div className="miu-portal">
<div className="miu-popover" style={{ top: pos.top, left: pos.left, ...vars } as CSSProperties}>
{children}
</div>
</div>,
document.body,
);
}
+7 -11
View File
@@ -196,13 +196,13 @@
.miu-react-btn:hover {
opacity: 1;
}
/* Portaled to <body> via .miu-popover so it's never clipped by a scroll container. */
.miu-popover {
position: fixed;
z-index: 2147483000;
transform: translateX(-100%); /* right-align the picker's right edge to the anchor */
}
.miu-react-picker {
position: absolute;
/* Open downward + right-aligned so it never clips against the top of the scroll area
(the reported bug) or spills past the right edge. */
top: 100%;
right: 0;
margin-top: 4px;
display: flex;
gap: 2px;
padding: 4px;
@@ -210,14 +210,9 @@
border: 1px solid var(--miu-border);
background: var(--miu-panel);
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.4);
z-index: 5;
width: max-content;
}
/* On my own (right-aligned) messages, anchor the picker to the left instead so it stays in view. */
.miu-msg.is-mine .miu-react-picker {
right: auto;
left: 0;
}
.miu-react-emoji {
border: none;
background: none;
@@ -836,6 +831,7 @@ button.miu-attach-dl:hover {
gap: 12px;
}
.miu-mail-msg {
flex: 0 0 auto; /* don't let the flex column shrink cards — they'd clip their own content */
border: 1px solid var(--miu-border);
border-radius: var(--miu-radius);
background: var(--miu-panel);
+4
View File
@@ -48,9 +48,13 @@ export interface Reaction {
}
export interface Attachment {
/** A resolved URL for display/download. May be empty until resolved by the host. */
url: string;
mime: string;
name: string;
/** Storage reference — carried so send() can persist the message part (upload returns it). */
contentRef?: string;
sizeBytes?: number;
}
export interface Message {