From 42491501004b840cc7c29ee16bdeb144c244d088 Mon Sep 17 00:00:00 2001 From: root Date: Sat, 16 May 2026 01:34:20 -0400 Subject: [PATCH] feat: reaction TDS styling, debounce read receipts, Escape to skip boot, type fixes - lotus-terminal.css.ts: add reaction chip styles for dark + light TDS modes (cyan border/bg for unselected, orange accent for own/pressed reactions) - useRoomReadPositions: debounce receipt handler at 150ms (M-3) - lotus-boot.ts: Escape key skips boot animation (I-3) - RoomInput.tsx: replace (uploadRes as any) with typed assertion (M-7) - CallEmbedProvider: call mention detection, audio cleanup, display name (C-1, C-2, M-5) - EventReaders: timestamps in seen-by modal, filter self, TDS styling - ReadReceiptAvatars: StackedAvatar pill, TDS visual treatment - chatBackground: add waves/neon/aurora backgrounds - RoomView: auto-apply tactical bg when TDS active and bg is none - settings: extend ChatBackground union type --- src/app/components/CallEmbedProvider.tsx | 15 +++- .../components/event-readers/EventReaders.tsx | 60 +++++++++---- .../ReadReceiptAvatars.tsx | 88 +++++++++++-------- src/app/features/lotus/chatBackground.ts | 66 ++++++++++++++ src/app/features/room/RoomInput.tsx | 2 +- src/app/features/room/RoomView.tsx | 3 +- src/app/hooks/useRoomReadPositions.ts | 32 ++++++- src/app/state/settings.ts | 2 +- src/lotus-boot.ts | 15 ++++ src/lotus-terminal.css.ts | 55 +++++++++++- 10 files changed, 270 insertions(+), 68 deletions(-) diff --git a/src/app/components/CallEmbedProvider.tsx b/src/app/components/CallEmbedProvider.tsx index 570f8d6b4..68c1d964f 100644 --- a/src/app/components/CallEmbedProvider.tsx +++ b/src/app/components/CallEmbedProvider.tsx @@ -49,7 +49,8 @@ import { useMediaAuthentication } from '../hooks/useMediaAuthentication'; import { mxcUrlToHttp } from '../utils/matrix'; import { RoomAvatar, RoomIcon } from './room-avatar'; import { useRoomNavigate } from '../hooks/useRoomNavigate'; -import { getStateEvent } from '../utils/room'; +import { getStateEvent, getMemberDisplayName } from '../utils/room'; +import { getMxIdLocalPart } from '../utils/matrix'; import { StateEvent } from '../../types/matrix/room'; import { getPowersLevelFromMatrixEvent } from '../hooks/usePowerLevels'; import { getRoomCreatorsForRoomId } from '../hooks/useRoomCreators'; @@ -119,13 +120,19 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr const playSound = useCallback(() => { const audioElement = audioRef.current; - audioElement?.play(); + audioElement?.play().catch(() => undefined); }, []); useEffect(() => { if (info.notificationType === 'ring') { playSound(); } + return () => { + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.currentTime = 0; + } + }; }, [playSound, info.notificationType]); useEffect(() => { @@ -150,7 +157,7 @@ function IncomingCall({ dm, info, onIgnore, onAnswer, onReject }: IncomingCallPr - {info.sender} + {getMemberDisplayName(info.room, info.sender) ?? getMxIdLocalPart(info.sender) ?? info.sender} @@ -287,7 +294,7 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps) const refEventId = relation?.event_id; const mention = - content['m.mentions'].room || content['m.mentions'].user_ids?.includes(mx.getSafeUserId()); + content['m.mentions']?.room || content['m.mentions']?.user_ids?.includes(mx.getSafeUserId()); if (!sender || !refEventId || !mention || Date.now() >= senderTs + lifetime) { return; } diff --git a/src/app/components/event-readers/EventReaders.tsx b/src/app/components/event-readers/EventReaders.tsx index c79002375..14d36fb28 100644 --- a/src/app/components/event-readers/EventReaders.tsx +++ b/src/app/components/event-readers/EventReaders.tsx @@ -16,7 +16,7 @@ import { import { Room } from 'matrix-js-sdk'; import { useRoomEventReaders } from '../../hooks/useRoomEventReaders'; import { getMemberDisplayName } from '../../utils/room'; -import { getMxIdLocalPart } from '../../utils/matrix'; +import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix'; import * as css from './EventReaders.css'; import { useMatrixClient } from '../../hooks/useMatrixClient'; import { UserAvatar } from '../user-avatar'; @@ -24,6 +24,19 @@ import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { useOpenUserRoomProfile } from '../../state/hooks/userRoomProfile'; import { useSpaceOptionally } from '../../hooks/useSpace'; import { getMouseEventCords } from '../../utils/dom'; +import { useSetting } from '../../state/hooks/settings'; +import { settingsAtom } from '../../state/settings'; +import { today, yesterday, timeHourMinute, timeMon, timeDay, timeYear } from '../../utils/time'; + +function formatReadTs(ts: number, hour24Clock: boolean): string { + const timeStr = timeHourMinute(ts, hour24Clock); + if (today(ts)) return `Today at ${timeStr}`; + if (yesterday(ts)) return `Yesterday at ${timeStr}`; + const sameYear = timeYear(ts) === timeYear(Date.now()); + return sameYear + ? `${timeMon(ts)} ${timeDay(ts)} at ${timeStr}` + : `${timeMon(ts)} ${timeDay(ts)} ${timeYear(ts)} at ${timeStr}`; +} export type EventReadersProps = { room: Room; @@ -34,9 +47,12 @@ export const EventReaders = as<'div', EventReadersProps>( ({ className, room, eventId, requestClose, ...props }, ref) => { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); - const latestEventReaders = useRoomEventReaders(room, eventId); + const myUserId = mx.getUserId(); + const latestEventReaders = useRoomEventReaders(room, eventId).filter((id) => id !== myUserId); const openProfile = useOpenUserRoomProfile(); const space = useSpaceOptionally(); + const [hour24Clock] = useSetting(settingsAtom, 'hour24Clock'); + const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal'); const getName = (userId: string) => getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId; @@ -48,9 +64,14 @@ export const EventReaders = as<'div', EventReadersProps>( {...props} ref={ref} > -
+
- Seen by + Seen by @@ -63,16 +84,9 @@ export const EventReaders = as<'div', EventReadersProps>( const name = getName(readerId); const avatarMxcUrl = room.getMember(readerId)?.getMxcAvatarUrl(); const avatarUrl = avatarMxcUrl - ? mx.mxcUrlToHttp( - avatarMxcUrl, - 100, - 100, - 'crop', - undefined, - false, - useAuthentication - ) + ? mxcUrlToHttp(mx, avatarMxcUrl, useAuthentication, 100, 100, 'crop') ?? undefined : undefined; + const receiptTs = room.getReadReceiptForUserId(readerId)?.data.ts; return ( ( } /> } > - - {name} - + + + {name} + + {receiptTs !== undefined && ( + + {formatReadTs(receiptTs, hour24Clock)} + + )} + ); })} diff --git a/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx b/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx index 03bea0d70..eec087186 100644 --- a/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx +++ b/src/app/components/read-receipt-avatars/ReadReceiptAvatars.tsx @@ -1,12 +1,15 @@ import React, { useState } from 'react'; import { Room } from 'matrix-js-sdk'; -import { Avatar, Icon, Icons, Modal, Overlay, OverlayBackdrop, OverlayCenter, Text } from 'folds'; +import { Icon, Icons, Modal, Overlay, OverlayBackdrop, OverlayCenter, Text, color } from 'folds'; import FocusTrap from 'focus-trap-react'; import { useMatrixClient } from '../../hooks/useMatrixClient'; +import { useSetting } from '../../state/hooks/settings'; +import { settingsAtom } from '../../state/settings'; import { useMediaAuthentication } from '../../hooks/useMediaAuthentication'; import { getMemberDisplayName } from '../../utils/room'; -import { getMxIdLocalPart } from '../../utils/matrix'; +import { getMxIdLocalPart, mxcUrlToHttp } from '../../utils/matrix'; import { UserAvatar } from '../user-avatar'; +import { StackedAvatar } from '../stacked-avatar'; import { EventReaders } from '../event-readers'; import { stopPropagation } from '../../utils/keyboard'; @@ -24,15 +27,17 @@ export function ReadReceiptAvatars({ const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const [open, setOpen] = useState(false); + const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal'); if (userIds.length === 0) return null; const displayed = userIds.slice(0, MAX_DISPLAY); const extra = userIds.length - MAX_DISPLAY; - const tooltipNames = userIds - .slice(0, 5) - .map((id) => getMemberDisplayName(room, id) ?? getMxIdLocalPart(id) ?? id) - .join(', ') + (extra > 0 ? ` +${extra} more` : ''); + const tooltipNames = + userIds + .slice(0, 5) + .map((id) => getMemberDisplayName(room, id) ?? getMxIdLocalPart(id) ?? id) + .join(', ') + (extra > 0 ? ` +${extra} more` : ''); return ( <> @@ -56,58 +61,65 @@ export function ReadReceiptAvatars({ type="button" onClick={() => setOpen(true)} title={tooltipNames} + aria-label={tooltipNames} style={{ background: 'none', border: 'none', cursor: 'pointer', - padding: '1px 0 0', + padding: 0, + marginLeft: 'auto', + marginTop: '4px', display: 'flex', alignItems: 'center', - gap: '3px', - marginLeft: 'auto', + gap: '4px', }} > - - {displayed.map((userId, i) => { + {/* Pill wrapper ensures visibility on any wallpaper/background */} + + {displayed.map((userId) => { const name = getMemberDisplayName(room, userId) ?? getMxIdLocalPart(userId) ?? userId; const avatarMxc = room.getMember(userId)?.getMxcAvatarUrl(); const avatarUrl = avatarMxc - ? mx.mxcUrlToHttp(avatarMxc, 32, 32, 'crop', undefined, false, useAuthentication) ?? - undefined + ? mxcUrlToHttp(mx, avatarMxc, useAuthentication, 32, 32, 'crop') ?? undefined : undefined; return ( - - - } - /> - - + } + /> + ); })} + {extra > 0 && ( + + +{extra} + + )} - {extra > 0 && ( - - +{extra} - - )} ); diff --git a/src/app/features/lotus/chatBackground.ts b/src/app/features/lotus/chatBackground.ts index 3a0552c9b..7190c63e4 100644 --- a/src/app/features/lotus/chatBackground.ts +++ b/src/app/features/lotus/chatBackground.ts @@ -16,6 +16,9 @@ export const BG_OPTIONS: { value: ChatBackground; label: string }[] = [ { value: 'tactical', label: 'Tactical' }, { value: 'circuit', label: 'Circuit' }, { value: 'hexgrid', label: 'Hex Grid' }, + { value: 'waves', label: 'Waves' }, + { value: 'neon', label: 'Neon Grid' }, + { value: 'aurora', label: 'Aurora' }, ]; const DARK: Record = { @@ -147,6 +150,39 @@ const DARK: Record = { backgroundImage: 'url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2229%22%20height%3D%2250%22%3E%3Cpath%20d%3D%22M14.5%200L29%208L29%2025L14.5%2033L0%2025L0%208Z%20M14.5%2033L29%2041V50%20M14.5%2033L0%2041V50%22%20fill%3D%22none%22%20stroke%3D%22rgba%280%2C212%2C255%2C0.13%29%22%20stroke-width%3D%220.8%22/%3E%3C/svg%3E")', backgroundSize: '29px 50px', }, + + // Flowing sine-wave lines + waves: { + backgroundColor: '#080c18', + backgroundImage: [ + 'repeating-radial-gradient(ellipse at 0% 50%, transparent 0, transparent 18px, rgba(80,130,255,0.07) 19px, transparent 20px)', + 'repeating-radial-gradient(ellipse at 100% 50%, transparent 0, transparent 28px, rgba(80,130,255,0.05) 29px, transparent 30px)', + 'repeating-radial-gradient(ellipse at 50% 0%, transparent 0, transparent 22px, rgba(100,60,200,0.06) 23px, transparent 24px)', + ].join(','), + }, + + // Neon cyberpunk grid — orange/cyan TDS colors + neon: { + backgroundColor: '#020408', + backgroundImage: [ + 'linear-gradient(rgba(255,107,0,0.10) 1px, transparent 1px)', + 'linear-gradient(90deg, rgba(255,107,0,0.10) 1px, transparent 1px)', + 'linear-gradient(rgba(0,212,255,0.05) 1px, transparent 1px)', + 'linear-gradient(90deg, rgba(0,212,255,0.05) 1px, transparent 1px)', + ].join(','), + backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px', + }, + + // Aurora borealis — flowing gradient bands + aurora: { + backgroundColor: '#030810', + backgroundImage: [ + 'radial-gradient(ellipse at 20% 30%, rgba(0,255,136,0.08) 0%, transparent 55%)', + 'radial-gradient(ellipse at 80% 70%, rgba(0,100,255,0.08) 0%, transparent 55%)', + 'radial-gradient(ellipse at 50% 10%, rgba(120,0,255,0.06) 0%, transparent 50%)', + 'radial-gradient(ellipse at 60% 90%, rgba(0,212,255,0.06) 0%, transparent 50%)', + ].join(','), + }, }; const LIGHT: Record = { @@ -272,6 +308,36 @@ const LIGHT: Record = { backgroundImage: 'url("data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2229%22%20height%3D%2250%22%3E%3Cpath%20d%3D%22M14.5%200L29%208L29%2025L14.5%2033L0%2025L0%208Z%20M14.5%2033L29%2041V50%20M14.5%2033L0%2041V50%22%20fill%3D%22none%22%20stroke%3D%22rgba%2850%2C100%2C220%2C0.11%29%22%20stroke-width%3D%220.8%22/%3E%3C/svg%3E")', backgroundSize: '29px 50px', }, + + waves: { + backgroundColor: '#eef3ff', + backgroundImage: [ + 'repeating-radial-gradient(ellipse at 0% 50%, transparent 0, transparent 18px, rgba(50,100,220,0.09) 19px, transparent 20px)', + 'repeating-radial-gradient(ellipse at 100% 50%, transparent 0, transparent 28px, rgba(50,100,220,0.07) 29px, transparent 30px)', + 'repeating-radial-gradient(ellipse at 50% 0%, transparent 0, transparent 22px, rgba(80,40,180,0.07) 23px, transparent 24px)', + ].join(','), + }, + + neon: { + backgroundColor: '#fafafa', + backgroundImage: [ + 'linear-gradient(rgba(196,78,0,0.12) 1px, transparent 1px)', + 'linear-gradient(90deg, rgba(196,78,0,0.12) 1px, transparent 1px)', + 'linear-gradient(rgba(0,98,184,0.06) 1px, transparent 1px)', + 'linear-gradient(90deg, rgba(0,98,184,0.06) 1px, transparent 1px)', + ].join(','), + backgroundSize: '60px 60px, 60px 60px, 12px 12px, 12px 12px', + }, + + aurora: { + backgroundColor: '#f4faf8', + backgroundImage: [ + 'radial-gradient(ellipse at 20% 30%, rgba(0,160,80,0.09) 0%, transparent 55%)', + 'radial-gradient(ellipse at 80% 70%, rgba(0,80,200,0.09) 0%, transparent 55%)', + 'radial-gradient(ellipse at 50% 10%, rgba(100,0,200,0.07) 0%, transparent 50%)', + 'radial-gradient(ellipse at 60% 90%, rgba(0,160,200,0.07) 0%, transparent 50%)', + ].join(','), + }, }; export const getChatBg = (bg: ChatBackground, isDark: boolean): CSSProperties => diff --git a/src/app/features/room/RoomInput.tsx b/src/app/features/room/RoomInput.tsx index aac26a076..0e77bf1c4 100644 --- a/src/app/features/room/RoomInput.tsx +++ b/src/app/features/room/RoomInput.tsx @@ -475,7 +475,7 @@ export const RoomInput = forwardRef( new File([blob], 'image.gif', { type: 'image/gif' }), { type: 'image/gif', name: 'image.gif', includeFilename: false } ); - const mxcUrl = (uploadRes as any).content_uri; + const mxcUrl = (uploadRes as { content_uri: string }).content_uri; if (!mxcUrl) return; mx.sendMessage(roomId, { msgtype: MsgType.Image, diff --git a/src/app/features/room/RoomView.tsx b/src/app/features/room/RoomView.tsx index 43da587e7..d8c306652 100644 --- a/src/app/features/room/RoomView.tsx +++ b/src/app/features/room/RoomView.tsx @@ -62,6 +62,7 @@ export function RoomView({ eventId }: { eventId?: string }) { const roomInputRef = useRef(null); const roomViewRef = useRef(null); const [chatBackground] = useSetting(settingsAtom, 'chatBackground'); + const [lotusTerminal] = useSetting(settingsAtom, 'lotusTerminal'); const theme = useTheme(); const isDark = theme.kind === ThemeKind.Dark; @@ -98,7 +99,7 @@ export function RoomView({ eventId }: { eventId?: string }) { ); return ( - + e.getId() === evtId); + if (idx === -1) return null; + for (let i = idx; i >= 0; i--) { + const e = liveEvents[i]; + if (!reactionOrEditEvent(e)) return e.getId() ?? null; + } + return null; +} function computePositions(room: Room, myUserId: string): Map { const map = new Map(); + const liveEvents = room.getLiveTimeline().getEvents(); for (const member of room.getJoinedMembers()) { if (member.userId === myUserId) continue; const evtId = room.getEventReadUpTo(member.userId); if (!evtId) continue; - const arr = map.get(evtId); + const targetId = nearestRenderableId(liveEvents, evtId); + if (!targetId) continue; + const arr = map.get(targetId); if (arr) arr.push(member.userId); - else map.set(evtId, [member.userId]); + else map.set(targetId, [member.userId]); } return map; } @@ -22,9 +38,17 @@ export function useRoomReadPositions(room: Room): Map { useEffect(() => { setPositions(computePositions(room, myUserId)); - const onReceipt = () => setPositions(computePositions(room, myUserId)); + let debounceTimer: ReturnType | null = null; + const onReceipt = (): void => { + if (debounceTimer !== null) clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + setPositions(computePositions(room, myUserId)); + debounceTimer = null; + }, 150); + }; room.on(RoomEvent.Receipt, onReceipt); return () => { + if (debounceTimer !== null) clearTimeout(debounceTimer); room.removeListener(RoomEvent.Receipt, onReceipt); }; }, [room, myUserId]); diff --git a/src/app/state/settings.ts b/src/app/state/settings.ts index 19ab27211..005b64227 100644 --- a/src/app/state/settings.ts +++ b/src/app/state/settings.ts @@ -9,7 +9,7 @@ export type DateFormat = | 'YYYY-MM-DD' | ''; export type MessageSpacing = '0' | '100' | '200' | '300' | '400' | '500'; -export type ChatBackground = 'none' | 'blueprint' | 'carbon' | 'stars' | 'topographic' | 'herringbone' | 'crosshatch' | 'chevron' | 'polka' | 'triangles' | 'plaid' | 'tactical' | 'circuit' | 'hexgrid'; +export type ChatBackground = 'none' | 'blueprint' | 'carbon' | 'stars' | 'topographic' | 'herringbone' | 'crosshatch' | 'chevron' | 'polka' | 'triangles' | 'plaid' | 'tactical' | 'circuit' | 'hexgrid' | 'waves' | 'neon' | 'aurora'; export enum MessageLayout { Modern = 0, Compact = 1, diff --git a/src/lotus-boot.ts b/src/lotus-boot.ts index 421a59901..23e1444e1 100644 --- a/src/lotus-boot.ts +++ b/src/lotus-boot.ts @@ -29,6 +29,7 @@ export function resetBootSequence(): void { export function runLotusBootSequence(force = false): void { if (!force && sessionStorage.getItem(STORAGE_KEY)) return; + if (document.getElementById('lt-boot')) return; sessionStorage.setItem(STORAGE_KEY, '1'); const overlay = document.createElement('div'); @@ -58,11 +59,25 @@ export function runLotusBootSequence(force = false): void { overlay.appendChild(pre); document.body.appendChild(overlay); + const dismiss = (): void => { + clearInterval(interval); + document.removeEventListener('keydown', onKey); + overlay.style.transition = 'opacity 0.4s ease'; + overlay.style.opacity = '0'; + setTimeout(() => overlay.remove(), 400); + }; + + const onKey = (e: KeyboardEvent): void => { + if (e.key === 'Escape') dismiss(); + }; + document.addEventListener('keydown', onKey); + let i = 0; let text = ''; const interval = setInterval(() => { if (i >= BOOT_MESSAGES.length) { clearInterval(interval); + document.removeEventListener('keydown', onKey); setTimeout(() => { overlay.style.transition = 'opacity 0.5s ease'; overlay.style.opacity = '0'; diff --git a/src/lotus-terminal.css.ts b/src/lotus-terminal.css.ts index 48ef26d48..3255516cd 100644 --- a/src/lotus-terminal.css.ts +++ b/src/lotus-terminal.css.ts @@ -325,8 +325,8 @@ globalStyle(`body.${lotusTerminalBodyClass} kbd`, { boxShadow: '0 1px 3px rgba(0,0,0,0.6)', }); -// Tooltip / title popups: if browser renders them, style if possible -globalStyle(`body.${lotusTerminalBodyClass} [title]`, { +// Tooltip / title popups: abbr only — avoid cursor:help on buttons/links +globalStyle(`body.${lotusTerminalBodyClass} abbr[title]`, { textDecoration: 'underline dotted rgba(0,212,255,0.35)', cursor: 'help', }); @@ -354,6 +354,32 @@ globalStyle(`body.${lotusTerminalBodyClass}`, { }); + +// ── Reaction chips (emoji reactions on messages) ──────────────────────────── +globalStyle(`body.${lotusTerminalBodyClass} button[data-reaction-key]`, { + backgroundColor: 'rgba(0,212,255,0.06)', + border: '1px solid rgba(0,212,255,0.22)', + borderRadius: '6px', + color: 'rgba(0,212,255,0.85)', + transition: 'background 0.12s, border-color 0.12s, box-shadow 0.12s', +}); +globalStyle(`body.${lotusTerminalBodyClass} button[data-reaction-key]:hover`, { + backgroundColor: 'rgba(0,212,255,0.13)', + borderColor: 'rgba(0,212,255,0.45)', + boxShadow: '0 0 8px rgba(0,212,255,0.18)', +}); +// Own reaction (aria-pressed = true) → orange accent +globalStyle(`body.${lotusTerminalBodyClass} button[data-reaction-key][aria-pressed="true"]`, { + backgroundColor: 'rgba(255,107,0,0.12)', + border: '1px solid rgba(255,107,0,0.38)', + color: 'rgba(255,140,0,0.90)', +}); +globalStyle(`body.${lotusTerminalBodyClass} button[data-reaction-key][aria-pressed="true"]:hover`, { + backgroundColor: 'rgba(255,107,0,0.20)', + borderColor: 'rgba(255,107,0,0.60)', + boxShadow: '0 0 8px rgba(255,107,0,0.22)', +}); + // ═══════════════════════════════════════════════════════════════════════════ // LIGHT MODE — TDS "daylight reading" variant // html[data-theme="light"] + body.lotusTerminalBodyClass @@ -575,3 +601,28 @@ globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} img:hover`, outline: '1px solid rgba(0,98,184,0.28)', boxShadow: '0 1px 8px rgba(0,98,184,0.08)', }); + +// Reaction chips — light TDS +globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key]`, { + backgroundColor: 'rgba(0,98,184,0.06)', + border: '1px solid rgba(0,98,184,0.22)', + borderRadius: '6px', + color: '#0062b8', + transition: 'background 0.12s, border-color 0.12s, box-shadow 0.12s', +}); +globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key]:hover`, { + backgroundColor: 'rgba(0,98,184,0.12)', + borderColor: 'rgba(0,98,184,0.42)', + boxShadow: '0 0 7px rgba(0,98,184,0.16)', +}); +globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key][aria-pressed="true"]`, { + backgroundColor: 'rgba(196,78,0,0.10)', + border: '1px solid rgba(196,78,0,0.35)', + color: '#c44e00', +}); +globalStyle(`html[data-theme="light"] body.${lotusTerminalBodyClass} button[data-reaction-key][aria-pressed="true"]:hover`, { + backgroundColor: 'rgba(196,78,0,0.18)', + borderColor: 'rgba(196,78,0,0.55)', + boxShadow: '0 0 7px rgba(196,78,0,0.18)', +}); +