vojo/src/app/features/bots/BotShell.css.ts

279 lines
8.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 { style } from '@vanilla-extract/css';
import { DefaultReset, color, toRem } from 'folds';
// BotShell is the bot-page container: it OWNS the hero and the iframe
// mount. Standard Cinny `RoomViewHeader` is intentionally absent here —
// the BotsDesktop mockup (stream-v2-dawn.jsx:660-672) places the hero as
// the first row of the bot panel, with no chat-style chrome above.
// Shell bg = SurfaceVariant.Container (#181a20) — matches DAWN.bg. The widget
// iframe body uses the same #181a20 (apps/widget-telegram/src/styles.css:8).
// Mockup canon paints the bot panel on DAWN.bg sitting on a DAWN.bg2
// (#0d0e11) parent — Background.Container would invert that and produce a
// visible seam at the iframe's top edge. SurfaceVariant.Container keeps the
// hero and the iframe body on the same tone.
export const Shell = style([
DefaultReset,
{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
backgroundColor: color.SurfaceVariant.Container,
overflow: 'hidden',
// Native safe-top: `#root` no longer reserves the status-bar inset
// (src/index.css), so BotShell extends to the screen top. The
// padding keeps the Hero (avatar + title + actions) clear of the
// system icons. Shell bg already matches the widget body tone, so
// the padding zone reads as a continuation of the bot surface. On
// web `--vojo-safe-top` is 0.
//
// Bottom inset is intentionally NOT added here: the iframe inside
// `Frame` paints its own body bg (#181a20, see widget-telegram
// styles.css) and the widget is responsible for the gesture-pill
// clearance of its own action rows. Padding Shell here exposed a
// visible seam between the iframe area and the env-bottom-tall
// strip below it on Android.
paddingTop: 'var(--vojo-safe-top, 0px)',
},
]);
export const Frame = style([
DefaultReset,
{
flex: 1,
minHeight: 0,
position: 'relative',
},
]);
// Hero outer band — full-width strip carrying the border-bottom that
// separates the hero from the iframe body. Vertical padding only; the
// horizontal padding sits on `HeroInner` so the inner content can be
// constrained to the same `max-width: 960px` the widget body uses
// (apps/widget-telegram/src/styles.css:64-72), keeping the host hero's
// left/right edges aligned with the body content visible inside the
// iframe.
export const Hero = style([
DefaultReset,
{
borderBottom: `1px solid ${color.Background.ContainerLine}`,
flexShrink: 0,
padding: `${toRem(36)} 0 ${toRem(28)}`,
'@media': {
// Compact mobile band — matches the visual height of Cinny's standard
// chat header (~56-64px), per user's request «хедер по разумеру как
// чат обычный». The big desktop hero (avatar 56 + 2-line title +
// multi-line description) is too heavy on a narrow viewport.
'(max-width: 600px)': {
padding: `${toRem(8)} 0`,
},
},
},
]);
// Inner row — constrained to 960px to match the widget body. Horizontal
// padding lives here. flex row carries the back-chevron / avatar / body /
// settings-button stack.
export const HeroInner = style([
DefaultReset,
{
maxWidth: toRem(960),
margin: '0 auto',
padding: `0 ${toRem(40)}`,
display: 'flex',
alignItems: 'flex-start',
gap: toRem(18),
'@media': {
'(max-width: 600px)': {
padding: `0 ${toRem(12)}`,
gap: toRem(10),
// Single row on mobile — no wrap. Avatar + name + settings fit
// as a chat-header-like strip; handle and description are hidden
// by their own media blocks below.
alignItems: 'center',
},
},
},
]);
// Mobile-only back chevron that lives at the start of the hero row. The
// hero re-orders to flex-start on mobile (hero already wraps via the
// max-width:600px media block), so the chevron leads the row rather than
// stacking awkwardly with the avatar.
export const HeroBack = style([
DefaultReset,
{
flexShrink: 0,
alignSelf: 'center',
},
]);
// 56×56 circular avatar, fleet violet (DAWN.fleet) bg. Fleet color is
// hardcoded here because it's the canonical bot accent in the mockup and we
// don't want it varying with Folds palette swaps. The violet disk shows when
// the bot's Matrix profile has no `avatar_url` (fallback to the initial
// letter); when it does, the inner <img> covers the violet — `overflow:
// hidden` keeps it inside the round mask.
export const HeroAvatar = style([
DefaultReset,
{
width: toRem(56),
height: toRem(56),
borderRadius: '50%',
backgroundColor: '#9580ff',
color: '#0c0c0e',
fontSize: toRem(24),
fontWeight: 700,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
flexShrink: 0,
overflow: 'hidden',
'@media': {
'(max-width: 600px)': {
width: toRem(36),
height: toRem(36),
fontSize: toRem(16),
},
},
},
]);
// Avatar image fills the violet square. `objectFit: cover` plus the
// container's `overflow: hidden` means non-square Matrix avatars (which
// arbitrarily sized) render correctly without showing the violet bg
// around the corners.
export const HeroAvatarImg = style([
DefaultReset,
{
width: '100%',
height: '100%',
objectFit: 'cover',
display: 'block',
},
]);
export const HeroBody = style([
DefaultReset,
{
flex: 1,
minWidth: 0,
},
]);
export const HeroTitleRow = style([
DefaultReset,
{
display: 'flex',
alignItems: 'baseline',
gap: toRem(10),
marginBottom: toRem(4),
flexWrap: 'wrap',
'@media': {
'(max-width: 600px)': {
marginBottom: 0,
},
},
},
]);
export const HeroName = style([
DefaultReset,
{
fontSize: toRem(22),
fontWeight: 700,
color: color.Surface.OnContainer,
'@media': {
'(max-width: 600px)': {
// Single-line truncated name — chat-header style.
fontSize: toRem(16),
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'block',
maxWidth: '100%',
},
},
},
]);
// Handle is desktop-only — mxid is rarely useful on phone where you can't
// easily copy it anyway, and the chat-header-style mobile band only has
// room for one row of metadata under the name (the short description,
// not the mxid).
export const HeroHandle = style([
DefaultReset,
{
fontSize: toRem(13),
color: color.SurfaceVariant.OnContainer,
fontFamily: 'ui-monospace, "JetBrains Mono", "SF Mono", monospace',
wordBreak: 'break-all',
opacity: 0.6,
'@media': {
'(max-width: 600px)': {
display: 'none',
},
},
},
]);
// Desktop description: full sentence(s), wraps freely up to 560px. Hidden
// on mobile in favor of the single-line `HeroDescriptionShort` below — the
// long copy doesn't fit in a chat-header band without pushing the avatar
// row out of vertical alignment.
export const HeroDescription = style([
DefaultReset,
{
fontSize: toRem(14),
// toRem keeps line-height in lockstep with font-size when the user
// scales root size or zooms; mixing raw px would break the 1.43 ratio.
lineHeight: toRem(20),
color: color.SurfaceVariant.OnContainer,
maxWidth: toRem(560),
'@media': {
'(max-width: 600px)': {
display: 'none',
},
},
},
]);
// Mobile-only one-liner description — sits directly under the name in the
// chat-header-style band. Truncated with ellipsis so a long short-desc
// from /config.json doesn't break the header height. Desktop hides this
// since the full-length `HeroDescription` carries the same content with
// more room.
export const HeroDescriptionShort = style([
DefaultReset,
{
display: 'none',
'@media': {
'(max-width: 600px)': {
display: 'block',
fontSize: toRem(12),
lineHeight: toRem(16),
color: color.SurfaceVariant.OnContainer,
opacity: 0.75,
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
maxWidth: '100%',
marginTop: toRem(2),
},
},
},
]);
// «О боте» button moved INSIDE the widget body (next to login/refresh/
// logout cards). The hero only keeps the standard three-dots IconButton
// as a trailing action — its styling comes from folds, no host-side CSS
// needed.