vojo/src/app/features/room/RoomView.css.ts

126 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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',
}
);