ui: forward dialog avatars, poll TDS, delivery icon, caption focus, boot hint
CI / Build & Quality Checks (push) Failing after 5m46s
CI / Build & Quality Checks (push) Failing after 5m46s
ForwardMessageDialog: - Room list now shows small avatars (48px crop) + DM label beneath room name - Forward is now async: spinner overlay while in-flight, '✓ Forwarded' only shown after sendEvent resolves; error clears sending state so user can retry - Search bar hidden in success state for cleaner confirmation view DeliveryStatus: - QUEUED state used ⏳ emoji breaking the ASCII/terminal aesthetic; changed to ⟳ matching the SENDING/ENCRYPTING icon PollContent: - Added data-poll-content + data-poll-answer + data-selected attributes so TDS CSS can override inline styles without JS branching - Added data-poll-content-label on the ◉ Poll header - TDS dark: answers get cyan dim bg/border, selected gets orange highlight with subtle box-shadow; hover brightens border; label uses cyan glow - TDS light: equivalent blue/orange variants Caption input: - Marked with data-caption-input; focus-visible ring added in index.css (blue for default, dark-theme dark blue) and lotus-terminal.css.ts (orange glow for TDS dark, orange for TDS light) Boot sequence: - Added '[ ESC ] skip' hint at bottom-right of overlay so users know they can dismiss it without waiting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -216,6 +216,7 @@ export function PollContent({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
data-poll-content
|
||||||
direction="Column"
|
direction="Column"
|
||||||
gap="200"
|
gap="200"
|
||||||
style={{ maxWidth: '340px', paddingTop: '2px', paddingBottom: '4px' }}
|
style={{ maxWidth: '340px', paddingTop: '2px', paddingBottom: '4px' }}
|
||||||
@@ -223,6 +224,7 @@ export function PollContent({
|
|||||||
<Box
|
<Box
|
||||||
alignItems="Center"
|
alignItems="Center"
|
||||||
gap="100"
|
gap="100"
|
||||||
|
data-poll-content-label
|
||||||
style={{
|
style={{
|
||||||
fontSize: '0.68rem',
|
fontSize: '0.68rem',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
@@ -251,6 +253,8 @@ export function PollContent({
|
|||||||
<button
|
<button
|
||||||
key={id}
|
key={id}
|
||||||
type="button"
|
type="button"
|
||||||
|
data-poll-answer
|
||||||
|
data-selected={selected}
|
||||||
onClick={canVote ? () => handleVote(id) : undefined}
|
onClick={canVote ? () => handleVote(id) : undefined}
|
||||||
style={{
|
style={{
|
||||||
padding: '7px 12px',
|
padding: '7px 12px',
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export function UploadCardRenderer({
|
|||||||
placeholder="Add a caption… (optional)"
|
placeholder="Add a caption… (optional)"
|
||||||
value={metadata.caption ?? ''}
|
value={metadata.caption ?? ''}
|
||||||
onChange={(e) => setMetadata(fileItem, { ...metadata, caption: e.target.value })}
|
onChange={(e) => setMetadata(fileItem, { ...metadata, caption: e.target.value })}
|
||||||
|
data-caption-input
|
||||||
style={{
|
style={{
|
||||||
marginTop: '6px',
|
marginTop: '6px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
@@ -199,6 +200,7 @@ export function UploadCardRenderer({
|
|||||||
color: 'var(--text-primary)',
|
color: 'var(--text-primary)',
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
|
transition: 'border-color 0.15s, box-shadow 0.15s',
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { ChangeEvent, useState } from 'react';
|
import React, { ChangeEvent, useCallback, useState } from 'react';
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import {
|
import {
|
||||||
|
Avatar,
|
||||||
Box,
|
Box,
|
||||||
config,
|
config,
|
||||||
Input,
|
Input,
|
||||||
@@ -11,11 +12,69 @@ import {
|
|||||||
OverlayBackdrop,
|
OverlayBackdrop,
|
||||||
OverlayCenter,
|
OverlayCenter,
|
||||||
Scroll,
|
Scroll,
|
||||||
|
Spinner,
|
||||||
Text,
|
Text,
|
||||||
} from 'folds';
|
} from 'folds';
|
||||||
import { MatrixEvent } from 'matrix-js-sdk';
|
import { MatrixEvent, Room } from 'matrix-js-sdk';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||||
import { stopPropagation } from '../../../utils/keyboard';
|
import { stopPropagation } from '../../../utils/keyboard';
|
||||||
|
import { mDirectAtom } from '../../../state/mDirectList';
|
||||||
|
import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication';
|
||||||
|
import { mxcUrlToHttp } from '../../../utils/matrix';
|
||||||
|
import { RoomAvatar, RoomIcon } from '../../../components/room-avatar';
|
||||||
|
|
||||||
|
type RoomRowProps = {
|
||||||
|
room: Room;
|
||||||
|
dm: boolean;
|
||||||
|
useAuthentication: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
sending: boolean;
|
||||||
|
};
|
||||||
|
function RoomRow({ room, dm, useAuthentication, onClick, sending }: RoomRowProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const avatarMxc = room.getMxcAvatarUrl();
|
||||||
|
const avatarUrl = avatarMxc
|
||||||
|
? (mxcUrlToHttp(mx, avatarMxc, useAuthentication, 48, 48, 'crop') ?? undefined)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={sending}
|
||||||
|
before={
|
||||||
|
<Avatar size="200" radii="300">
|
||||||
|
<RoomAvatar
|
||||||
|
roomId={room.roomId}
|
||||||
|
src={avatarUrl}
|
||||||
|
alt={room.name}
|
||||||
|
renderFallback={() => (
|
||||||
|
<RoomIcon
|
||||||
|
roomType={room.getType()}
|
||||||
|
size="100"
|
||||||
|
joinRule={room.getJoinRule()}
|
||||||
|
filled
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box direction="Column">
|
||||||
|
<Text size="T300" truncate>
|
||||||
|
{room.name}
|
||||||
|
</Text>
|
||||||
|
{dm && (
|
||||||
|
<Text size="T200" priority="300" truncate>
|
||||||
|
Direct Message
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
mEvent: MatrixEvent;
|
mEvent: MatrixEvent;
|
||||||
@@ -24,7 +83,10 @@ type Props = {
|
|||||||
|
|
||||||
export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
const directs = useAtomValue(mDirectAtom);
|
||||||
|
const useAuthentication = useMediaAuthentication();
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
|
const [sending, setSending] = useState(false);
|
||||||
const [sentTo, setSentTo] = useState<string | null>(null);
|
const [sentTo, setSentTo] = useState<string | null>(null);
|
||||||
|
|
||||||
const allRooms = mx
|
const allRooms = mx
|
||||||
@@ -36,14 +98,23 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
|||||||
? allRooms.filter((r) => r.name.toLowerCase().includes(query.toLowerCase()))
|
? allRooms.filter((r) => r.name.toLowerCase().includes(query.toLowerCase()))
|
||||||
: allRooms;
|
: allRooms;
|
||||||
|
|
||||||
const forward = (roomId: string, roomName: string) => {
|
const forward = useCallback(
|
||||||
const fwdContent: Record<string, unknown> = { ...mEvent.getContent() };
|
async (room: Room) => {
|
||||||
delete fwdContent['m.relates_to'];
|
if (sending) return;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
setSending(true);
|
||||||
(mx as any).sendEvent(roomId, mEvent.getType(), fwdContent);
|
const fwdContent: Record<string, unknown> = { ...mEvent.getContent() };
|
||||||
setSentTo(roomName);
|
delete fwdContent['m.relates_to'];
|
||||||
setTimeout(onClose, 1200);
|
try {
|
||||||
};
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
await (mx as any).sendEvent(room.roomId, mEvent.getType(), fwdContent);
|
||||||
|
setSentTo(room.name);
|
||||||
|
setTimeout(onClose, 1400);
|
||||||
|
} catch {
|
||||||
|
setSending(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[mx, mEvent, onClose, sending],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay open backdrop={<OverlayBackdrop />}>
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||||
@@ -59,7 +130,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
|||||||
<Modal
|
<Modal
|
||||||
size="400"
|
size="400"
|
||||||
style={{
|
style={{
|
||||||
maxHeight: '440px',
|
maxHeight: '480px',
|
||||||
borderRadius: config.radii.R500,
|
borderRadius: config.radii.R500,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
@@ -72,15 +143,17 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
|||||||
style={{ padding: config.space.S400, paddingBottom: config.space.S200 }}
|
style={{ padding: config.space.S400, paddingBottom: config.space.S200 }}
|
||||||
>
|
>
|
||||||
<Text size="H5">Forward message</Text>
|
<Text size="H5">Forward message</Text>
|
||||||
<Input
|
{!sentTo && (
|
||||||
variant="Background"
|
<Input
|
||||||
size="400"
|
variant="Background"
|
||||||
radii="400"
|
size="400"
|
||||||
outlined
|
radii="400"
|
||||||
placeholder="Search rooms…"
|
outlined
|
||||||
value={query}
|
placeholder="Search rooms…"
|
||||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)}
|
value={query}
|
||||||
/>
|
onChange={(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Line size="300" />
|
<Line size="300" />
|
||||||
{sentTo ? (
|
{sentTo ? (
|
||||||
@@ -88,6 +161,7 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
|||||||
grow="Yes"
|
grow="Yes"
|
||||||
alignItems="Center"
|
alignItems="Center"
|
||||||
justifyContent="Center"
|
justifyContent="Center"
|
||||||
|
gap="300"
|
||||||
style={{ padding: config.space.S400 }}
|
style={{ padding: config.space.S400 }}
|
||||||
>
|
>
|
||||||
<Text size="T300">✓ Forwarded to {sentTo}</Text>
|
<Text size="T300">✓ Forwarded to {sentTo}</Text>
|
||||||
@@ -97,16 +171,14 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
|||||||
<Scroll size="300" hideTrack visibility="Hover">
|
<Scroll size="300" hideTrack visibility="Hover">
|
||||||
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
<Box direction="Column" gap="100" style={{ padding: config.space.S200 }}>
|
||||||
{filtered.slice(0, 60).map((room) => (
|
{filtered.slice(0, 60).map((room) => (
|
||||||
<MenuItem
|
<RoomRow
|
||||||
key={room.roomId}
|
key={room.roomId}
|
||||||
size="300"
|
room={room}
|
||||||
radii="300"
|
dm={directs.has(room.roomId)}
|
||||||
onClick={() => forward(room.roomId, room.name)}
|
useAuthentication={useAuthentication}
|
||||||
>
|
onClick={() => forward(room)}
|
||||||
<Text size="T300" truncate>
|
sending={sending}
|
||||||
{room.name}
|
/>
|
||||||
</Text>
|
|
||||||
</MenuItem>
|
|
||||||
))}
|
))}
|
||||||
{filtered.length === 0 && (
|
{filtered.length === 0 && (
|
||||||
<Box
|
<Box
|
||||||
@@ -121,6 +193,20 @@ export function ForwardMessageDialog({ mEvent, onClose }: Props) {
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Scroll>
|
</Scroll>
|
||||||
|
{sending && (
|
||||||
|
<Box
|
||||||
|
alignItems="Center"
|
||||||
|
justifyContent="Center"
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
|
background: 'rgba(0,0,0,0.18)',
|
||||||
|
borderRadius: config.radii.R500,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Spinner variant="Secondary" size="400" />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ function DeliveryStatus({
|
|||||||
label = 'Failed to send';
|
label = 'Failed to send';
|
||||||
colorStyle = lotusTerminal ? '#FF3B3B' : color.Critical.Main;
|
colorStyle = lotusTerminal ? '#FF3B3B' : color.Critical.Main;
|
||||||
} else if (status === EventStatus.QUEUED) {
|
} else if (status === EventStatus.QUEUED) {
|
||||||
icon = '⏳';
|
icon = '⟳';
|
||||||
label = 'Queued';
|
label = 'Queued';
|
||||||
colorStyle = lotusTerminal ? 'rgba(0,212,255,0.45)' : color.Secondary.Main;
|
colorStyle = lotusTerminal ? 'rgba(0,212,255,0.45)' : color.Secondary.Main;
|
||||||
} else if (status === EventStatus.SENDING || status === EventStatus.ENCRYPTING) {
|
} else if (status === EventStatus.SENDING || status === EventStatus.ENCRYPTING) {
|
||||||
|
|||||||
@@ -141,6 +141,17 @@ textarea {
|
|||||||
word-spacing: inherit;
|
word-spacing: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Caption input focus ring — replaces stripped outline */
|
||||||
|
[data-caption-input]:focus-visible {
|
||||||
|
border-color: rgba(0, 100, 200, 0.55);
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 100, 200, 0.18);
|
||||||
|
}
|
||||||
|
.dark-theme [data-caption-input]:focus-visible,
|
||||||
|
.butter-theme [data-caption-input]:focus-visible {
|
||||||
|
border-color: rgba(100, 160, 255, 0.60);
|
||||||
|
box-shadow: 0 0 0 2px rgba(100, 160, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
audio:not([controls]) {
|
audio:not([controls]) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,8 +55,25 @@ export function runLotusBootSequence(force = false): void {
|
|||||||
'line-height:1.7',
|
'line-height:1.7',
|
||||||
'white-space:pre-wrap',
|
'white-space:pre-wrap',
|
||||||
'overflow:hidden',
|
'overflow:hidden',
|
||||||
|
'flex:1',
|
||||||
].join(';');
|
].join(';');
|
||||||
overlay.appendChild(pre);
|
overlay.appendChild(pre);
|
||||||
|
|
||||||
|
const escHint = document.createElement('div');
|
||||||
|
escHint.style.cssText = [
|
||||||
|
'position:absolute',
|
||||||
|
'bottom:1.5rem',
|
||||||
|
'right:2rem',
|
||||||
|
'font-size:0.68rem',
|
||||||
|
'color:rgba(0,255,136,0.35)',
|
||||||
|
"font-family:'JetBrains Mono','Courier New',monospace",
|
||||||
|
'letter-spacing:0.08em',
|
||||||
|
'user-select:none',
|
||||||
|
'pointer-events:none',
|
||||||
|
].join(';');
|
||||||
|
escHint.textContent = '[ ESC ] skip';
|
||||||
|
overlay.appendChild(escHint);
|
||||||
|
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
const dismiss = (): void => {
|
const dismiss = (): void => {
|
||||||
|
|||||||
@@ -878,3 +878,55 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} ._13tt0gb6:
|
|||||||
background: 'rgba(0,98,184,0.08) !important' as any,
|
background: 'rgba(0,98,184,0.08) !important' as any,
|
||||||
color: '#0062b8 !important' as any,
|
color: '#0062b8 !important' as any,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Poll card TDS ─────────────────────────────────────────────────────────────
|
||||||
|
globalStyle(`body.${lotusTerminalBodyClass} [data-poll-content] [data-poll-answer]`, {
|
||||||
|
background: 'rgba(0,212,255,0.04) !important' as any,
|
||||||
|
border: '1px solid rgba(0,212,255,0.22) !important' as any,
|
||||||
|
color: '#c4d9ee !important' as any,
|
||||||
|
transition: 'border-color 0.15s, background 0.15s, box-shadow 0.15s',
|
||||||
|
});
|
||||||
|
globalStyle(`body.${lotusTerminalBodyClass} [data-poll-content] [data-poll-answer]:hover`, {
|
||||||
|
background: 'rgba(0,212,255,0.08) !important' as any,
|
||||||
|
borderColor: 'rgba(0,212,255,0.40) !important' as any,
|
||||||
|
boxShadow: '0 0 8px rgba(0,212,255,0.08)',
|
||||||
|
});
|
||||||
|
globalStyle(
|
||||||
|
`body.${lotusTerminalBodyClass} [data-poll-content] [data-poll-answer][data-selected="true"]`,
|
||||||
|
{
|
||||||
|
background: 'rgba(255,107,0,0.10) !important' as any,
|
||||||
|
border: '1px solid rgba(255,107,0,0.55) !important' as any,
|
||||||
|
boxShadow: '0 0 10px rgba(255,107,0,0.10)',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
globalStyle(`body.${lotusTerminalBodyClass} [data-poll-content] [data-poll-content-label]`, {
|
||||||
|
color: 'rgba(0,212,255,0.60) !important' as any,
|
||||||
|
opacity: '1 !important' as any,
|
||||||
|
textShadow: '0 0 8px rgba(0,212,255,0.25)',
|
||||||
|
});
|
||||||
|
// light TDS
|
||||||
|
globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-poll-content] [data-poll-answer]`, {
|
||||||
|
background: 'rgba(0,98,184,0.04) !important' as any,
|
||||||
|
border: '1px solid rgba(0,98,184,0.22) !important' as any,
|
||||||
|
color: '#111827 !important' as any,
|
||||||
|
});
|
||||||
|
globalStyle(
|
||||||
|
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-poll-content] [data-poll-answer][data-selected="true"]`,
|
||||||
|
{
|
||||||
|
background: 'rgba(196,78,0,0.08) !important' as any,
|
||||||
|
border: '1px solid rgba(196,78,0,0.50) !important' as any,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Caption input TDS focus ring ──────────────────────────────────────────────
|
||||||
|
globalStyle(`body.${lotusTerminalBodyClass} [data-caption-input]:focus-visible`, {
|
||||||
|
borderColor: 'rgba(255,107,0,0.65) !important' as any,
|
||||||
|
boxShadow: '0 0 0 2px rgba(255,107,0,0.14), 0 0 6px rgba(255,107,0,0.10) !important' as any,
|
||||||
|
});
|
||||||
|
globalStyle(
|
||||||
|
`html[data-theme="light"] body.${lotusTerminalBodyClass} [data-caption-input]:focus-visible`,
|
||||||
|
{
|
||||||
|
borderColor: 'rgba(196,78,0,0.60) !important' as any,
|
||||||
|
boxShadow: '0 0 0 2px rgba(196,78,0,0.12) !important' as any,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user