import { globalStyle, style } from '@vanilla-extract/css'; import { color, toRem } from 'folds'; import { Editor, EditorTextarea, EditorTextareaScroll, } from '../../components/editor/Editor.css'; import { VOJO_HORSESHOE_RADIUS_PX } from '../../styles/horseshoe'; // Main chat composer — Dawn canon (stream-v2-dawn.jsx line 285-307): a // floating dark card with all-corners rounded geometry. Two-row layout // (textarea on top, action strip with `+` / emoji / send below) — same // shape on every platform. Radius matches the silhouette horseshoe at the // top of the chat (32px) for visual harmony — top and bottom of the chat // surface mirror the same curvature. The thread-drawer composer in // `ThreadDrawer.tsx` also wraps `RoomInput` with this class // (`${ThreadComposer} ${ChatComposer}`), so it inherits both the dark // card chrome and the compact two-row geometry. The message-edit overlay // and `Editor.preview.tsx` mount `CustomEditor` directly without the // `ChatComposer` wrap, so they keep the folds-default pill R400 + // SurfaceVariant fill. The touch-hover gate at the bottom of this file // also covers `RoomTombstone` / `RoomInputPlaceholder` since they share // the wrap, which is intentional: their action buttons benefit from the // same Android-WebView stuck-:hover suppression. export const ChatComposer = style({}); // Outer absolute-positioned wrapper for the composer overlay. Carries the // slide/fade transition driven by the `data-hidden` attribute set from // React state. CSS class (not inline `transition`) so the // `prefers-reduced-motion` media query can disable the motion. export const ComposerOverlay = style({ position: 'absolute', left: 0, right: 0, bottom: 0, zIndex: 10, transition: 'transform 220ms ease-out, opacity 220ms ease-out', willChange: 'transform, opacity', selectors: { '&[data-hidden="true"]': { transform: 'translateY(120%)', opacity: 0, pointerEvents: 'none', }, }, '@media': { '(prefers-reduced-motion: reduce)': { transition: 'none', }, }, }); globalStyle(`${ChatComposer} .${Editor}`, { backgroundColor: color.Surface.Container, borderRadius: toRem(VOJO_HORSESHOE_RADIUS_PX), boxShadow: 'none', // Asymmetric outer buffer for the 32px corner-radius card: // * Horizontal 16px — keeps the placeholder + Plus column visually // clear of the side curves. // * Vertical 6px — minimum that still leaves comfortable clearance // from the top/bottom curves while keeping the card compact. // Bottom-left geometry check against the 32px corner curve (centre at // (32, card_bottom-32)) with action-row `padding: 2 8 4` (RoomInput.tsx): // * button-bottom y-from-card-bottom = 6 (outer) + 4 (row pad-bot) = 10 // * curve-x at y=10 = 32 − √(32² − 22²) ≈ 8.76px // * button-left from card-left = 16 (outer) + 8 (row pad-left) = 24 // → ~15px clearance between the button hit-box and the curve. // Top-left mirror (curve at y=19 from top is ~2.8px from left, text-left // at 28px → ~25px clearance) is even more generous, which fits the // single-line composer that grows downward when wrapping. // The same math is consolidated in docs/ai/architecture.md (Composer // card geometry section) — keep that table in sync if you tune these. padding: `${toRem(6)} ${toRem(16)}`, }); // Visual alignment goal: typed-text glyph-start and Plus-icon glyph-start // sit on the same vertical column at 28px from the card edge (mirrored on // the right for Send). One column line through "Написать сообщение…" and // the leading Plus. // text-glyph-start = outer (16) + textarea paddingLeft (12) = 28 // icon-glyph-left = outer (16) + row paddingLeft (8) + button pad (4) = 28 globalStyle(`${ChatComposer} ${EditorTextareaScroll}:first-child ${EditorTextarea}`, { paddingLeft: toRem(12), }); globalStyle(`${ChatComposer} ${EditorTextareaScroll}:last-child ${EditorTextarea}`, { paddingRight: toRem(12), }); // NB: do NOT override `EditorTextarea.paddingTop` / `paddingBottom` here. // Folds tuned the textarea's 13px vertical padding to MATCH the // `EditorPlaceholderTextVisual.paddingTop` (also 13px) so the placeholder // span and the typed-text glyph land on the same y inside the editable // content-box. Changing only the textarea padding without retuning the // placeholder visual produces a visible mismatch — the caret/typed text // drifts vertically relative to the «Send a message…» placeholder. If // height compactness is needed in the future, the safer levers are: // * outer card vertical padding (the `padding: 6 16` above) // * action-row padding (set in RoomInput.tsx `bottom` slot) // — both untie cleanly from Slate's placeholder positioning. // NB: do NOT override `EditorPlaceholderTextVisual.paddingLeft` here. Slate // renders the placeholder span absolutely-positioned (`position: absolute; // top: 0`) inside the leaf text node — its origin is the contenteditable // content-box, already shifted by the textarea paddingLeft above. An // additional paddingLeft here would visually shift the placeholder right of // the real caret. The `Editor.css.ts` default (paddingLeft: 1px) keeps the // placeholder glyph one pixel right of the caret — virtually co-located. // Suppress folds' `IconButton :hover / :focus-visible` background paint on // touch sessions. Capacitor's Android WebView synthesises both pseudoclasses // on the tapped element after release and never clears them until the next // interaction elsewhere — without this gate the action-row buttons stay // highlighted in grey after every tap. Mirrors the widget-telegram fix // (apps/widget-telegram/src/styles.css line 247-272) but scoped to the chat // composer so we don't regress hover affordances anywhere else. The input // mode comes from `src/index.tsx`'s `:root[data-input]` attribute, which is // the only honest signal on Android WebView (matchMedia hover/pointer // queries lie there). NB: only `:focus-visible` is gated — plain `:focus` // is left alone so a hybrid device (touch screen + Bluetooth keyboard) // keeps the focus ring during keyboard tab-traversal even after a touch // session is detected. globalStyle( `:root[data-input="touch"] ${ChatComposer} button:hover, :root[data-input="touch"] ${ChatComposer} button:focus-visible`, { backgroundColor: 'transparent', } );