279 lines
8.3 KiB
TypeScript
279 lines
8.3 KiB
TypeScript
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.
|