126 lines
6.3 KiB
TypeScript
126 lines
6.3 KiB
TypeScript
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',
|
||
}
|
||
);
|