feat(composer): tighten action-row padding, extract button JSX, and rotate placeholder across 12 hour-keyed variants
This commit is contained in:
parent
646cb7b124
commit
8e2db986b4
5 changed files with 235 additions and 95 deletions
|
|
@ -167,6 +167,51 @@ Stock Cinny had multiple themes; vojo simplified to System / Light / Dark (commi
|
||||||
- Folds tokens (`color.*`, `config.space`, `config.radii`, `config.borderWidth`) are read-only inside folds compiled CSS. Re-skinning colours via `createTheme()` works; re-skinning radii or spacing requires CSS overrides outside folds.
|
- Folds tokens (`color.*`, `config.space`, `config.radii`, `config.borderWidth`) are read-only inside folds compiled CSS. Re-skinning colours via `createTheme()` works; re-skinning radii or spacing requires CSS overrides outside folds.
|
||||||
- Brand accent in v4.11.x: `Primary.Main = #BDB6EC` (lavender) — referenced in unread-badge, focus-ring, NavLink active state, MessageBase highlight keyframe.
|
- Brand accent in v4.11.x: `Primary.Main = #BDB6EC` (lavender) — referenced in unread-badge, focus-ring, NavLink active state, MessageBase highlight keyframe.
|
||||||
|
|
||||||
|
## Composer card geometry
|
||||||
|
|
||||||
|
Load-bearing pixel values for the main chat composer + thread-drawer composer (both wrap `RoomInput` with the `ChatComposer` class). The composer is a floating rounded card with **32px corner radius** (`VOJO_HORSESHOE_RADIUS_PX`); all paddings are tuned so the visible glyphs (text, IconButton icons) stay outside the curve clip. Source of truth: [`src/app/features/room/RoomView.css.ts`](../../src/app/features/room/RoomView.css.ts), [`src/app/features/room/RoomInput.tsx`](../../src/app/features/room/RoomInput.tsx) (action-row padding).
|
||||||
|
|
||||||
|
| Element | Value | Where |
|
||||||
|
|---|---|---|
|
||||||
|
| Card corner radius | 32px | `VOJO_HORSESHOE_RADIUS_PX` |
|
||||||
|
| Card outer padding | `6px / 16px` (vertical / horizontal) | `RoomView.css.ts` → `.ChatComposer .Editor` |
|
||||||
|
| Textarea vertical padding | 13px (folds default — do NOT override) | `Editor.css.ts` → `EditorTextarea` |
|
||||||
|
| Textarea horizontal padding | 12px left, 12px right | `RoomView.css.ts` → `:first-child` / `:last-child` rules |
|
||||||
|
| Placeholder paddingTop | 13px (folds default — must match textarea padding) | `Editor.css.ts` → `EditorPlaceholderTextVisual` |
|
||||||
|
| Action-row padding | `2px / 8px / 4px` (top / sides / bottom) | `RoomInput.tsx` `bottom` slot |
|
||||||
|
| IconButton size | 32×32 (folds `size="300"`, `fill="None"`) | `RoomInput.tsx` |
|
||||||
|
| IconButton internal padding | 4px (SVG 24×24 centered) | folds default |
|
||||||
|
| Empty-state composer height (single-line, no reply) | ~93px | derived |
|
||||||
|
|
||||||
|
**Don't override the textarea's vertical padding (13px) without also retuning `EditorPlaceholderTextVisual.paddingTop` in lockstep**: folds tuned the pair so Slate's placeholder span and the typed-text caret land on the same y inside the contenteditable content-box. Diverging the two breaks vertical alignment — typed text and the «Send a message…» placeholder appear at different baselines.
|
||||||
|
|
||||||
|
**Visual alignment goal** — text glyph and Plus icon-glyph sit on the same vertical column at 28px from the card edge (mirrored on the right for Send):
|
||||||
|
- `text-glyph-x = outer (16) + textarea paddingLeft (12) = 28`
|
||||||
|
- `icon-glyph-x = outer (16) + row paddingLeft (8) + button-internal-pad (4) = 28`
|
||||||
|
|
||||||
|
**Bottom-left curve clearance** (Plus IconButton container vs the 32px corner):
|
||||||
|
- `button-bottom y = 6 (outer) + 4 (row pad-bot) = 10`
|
||||||
|
- `curve-x at y=10 = 32 − √(32² − 22²) ≈ 8.76px`
|
||||||
|
- `button-left = 16 (outer) + 8 (row pad-left) = 24`
|
||||||
|
- **clearance ≈ 15.24px** — comfortable for the hit-box; the visible glyph clears the curve by ~23px
|
||||||
|
|
||||||
|
**Top-left curve clearance** (placeholder text glyph):
|
||||||
|
- `text-glyph-y = 6 (outer) + 13 (textarea pad-top) = 19`
|
||||||
|
- `curve-x at y=19 = 32 − √(32² − 13²) ≈ 2.76px`
|
||||||
|
- `text-glyph-x = 28`
|
||||||
|
- **clearance ≈ 25.24px** — very generous; supports multi-line growth
|
||||||
|
|
||||||
|
**Future compactness levers** (if needed without breaking alignment):
|
||||||
|
- Outer card vertical padding (currently 6px) — drop to 4px saves 4px
|
||||||
|
- Action-row padding (currently 2/4) — drop to 0/2 saves 4px
|
||||||
|
- IconButton size (currently 300 / 32px) — already smallest in folds; no further reduction available
|
||||||
|
|
||||||
|
Avoid touching textarea or placeholder vertical padding unless you re-tune both in matched pairs and visually verify glyph alignment.
|
||||||
|
|
||||||
|
**Don't apply these to other composers**: the textarea-padding compact override is scoped to `.ChatComposer`. The message-edit overlay, `Editor.preview.tsx`, and any future `CustomEditor` consumer outside the chat composer keep the folds-default `padding: 13px 1px` (`Editor.css.ts:24-42`).
|
||||||
|
|
||||||
|
If you re-tune any number here, update both the CSS comments in `RoomView.css.ts` and this table — they're cross-referenced.
|
||||||
|
|
||||||
## Responsive design
|
## Responsive design
|
||||||
|
|
||||||
**No CSS media queries** for layout. Responsive behaviour goes through `hooks/useScreenSize.ts`:
|
**No CSS media queries** for layout. Responsive behaviour goes through `hooks/useScreenSize.ts`:
|
||||||
|
|
|
||||||
|
|
@ -510,6 +510,17 @@
|
||||||
"leave_room": "Leave Room",
|
"leave_room": "Leave Room",
|
||||||
|
|
||||||
"send_message": "Send a message...",
|
"send_message": "Send a message...",
|
||||||
|
"send_message_alt_1": "One line or many...",
|
||||||
|
"send_message_alt_2": "Write something right now...",
|
||||||
|
"send_message_alt_3": "Don't keep me waiting, type...",
|
||||||
|
"send_message_alt_4": "This line won't fill itself...",
|
||||||
|
"send_message_alt_5": "So... what's it gonna be?..",
|
||||||
|
"send_message_alt_6": "Nobody reads placeholders. But you did...",
|
||||||
|
"send_message_alt_7": "Letters here, please...",
|
||||||
|
"send_message_alt_8": "You stare at the placeholder. The placeholder stares back...",
|
||||||
|
"send_message_alt_9": "Congrats, you're in the 3% who read placeholders...",
|
||||||
|
"send_message_alt_10": "Fine, I'll wait... and wait...",
|
||||||
|
"send_message_alt_11": "After you...",
|
||||||
"drop_files": "Drop Files in \"{{name}}\"",
|
"drop_files": "Drop Files in \"{{name}}\"",
|
||||||
"drag_drop_desc": "Drag and drop files here or click for selection dialog",
|
"drag_drop_desc": "Drag and drop files here or click for selection dialog",
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -518,6 +518,17 @@
|
||||||
"leave_room": "Покинуть комнату",
|
"leave_room": "Покинуть комнату",
|
||||||
|
|
||||||
"send_message": "Написать сообщение...",
|
"send_message": "Написать сообщение...",
|
||||||
|
"send_message_alt_1": "В одну строку или несколько...",
|
||||||
|
"send_message_alt_2": "Написать в эту минуту...",
|
||||||
|
"send_message_alt_3": "Не томи, пиши...",
|
||||||
|
"send_message_alt_4": "Эта строка сама себя не заполнит...",
|
||||||
|
"send_message_alt_5": "Ну так что?..",
|
||||||
|
"send_message_alt_6": "Никто не читает плейсхолдеры. Но вы прочитали...",
|
||||||
|
"send_message_alt_7": "Сюда буквы, пожалуйста...",
|
||||||
|
"send_message_alt_8": "Вы смотрите на плейсхолдер. Плейсхолдер смотрит на вас...",
|
||||||
|
"send_message_alt_9": "Поздравляю, вы в 3% людей, читающих плейсхолдеры...",
|
||||||
|
"send_message_alt_10": "Ну я подожду, подожду...",
|
||||||
|
"send_message_alt_11": "Только после вас...",
|
||||||
"drop_files": "Перетащите файлы в \"{{name}}\"",
|
"drop_files": "Перетащите файлы в \"{{name}}\"",
|
||||||
"drag_drop_desc": "Перетащите файлы сюда или нажмите для выбора",
|
"drag_drop_desc": "Перетащите файлы сюда или нажмите для выбора",
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import React, {
|
import React, {
|
||||||
KeyboardEventHandler,
|
KeyboardEventHandler,
|
||||||
|
MouseEvent as ReactMouseEvent,
|
||||||
RefObject,
|
RefObject,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
@ -117,6 +119,28 @@ import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
|
||||||
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
|
import { usePowerLevelTags } from '../../hooks/usePowerLevelTags';
|
||||||
import { useComposingCheck } from '../../hooks/useComposingCheck';
|
import { useComposingCheck } from '../../hooks/useComposingCheck';
|
||||||
|
|
||||||
|
// Placeholder rotation set — 12 i18n keys (1 default + 11 alternates) under
|
||||||
|
// the `Room` namespace; the hour-of-day slot picks one. With 12 variants
|
||||||
|
// over 24 hours each variant occupies two specific hour slots per day, so
|
||||||
|
// the placeholder is stable within an hour and a user is most likely to
|
||||||
|
// notice the change when they navigate to another room across an hour
|
||||||
|
// boundary. Keep this list in sync with `public/locales/{en,ru}.json`
|
||||||
|
// `Room.send_message*` entries.
|
||||||
|
const COMPOSER_PLACEHOLDER_KEYS = [
|
||||||
|
'Room.send_message',
|
||||||
|
'Room.send_message_alt_1',
|
||||||
|
'Room.send_message_alt_2',
|
||||||
|
'Room.send_message_alt_3',
|
||||||
|
'Room.send_message_alt_4',
|
||||||
|
'Room.send_message_alt_5',
|
||||||
|
'Room.send_message_alt_6',
|
||||||
|
'Room.send_message_alt_7',
|
||||||
|
'Room.send_message_alt_8',
|
||||||
|
'Room.send_message_alt_9',
|
||||||
|
'Room.send_message_alt_10',
|
||||||
|
'Room.send_message_alt_11',
|
||||||
|
] as const;
|
||||||
|
|
||||||
// Composer action-row icons — stroke-based outline style from the Dawn
|
// Composer action-row icons — stroke-based outline style from the Dawn
|
||||||
// canon (docs/design/new-direct-messages-design/project/shared.jsx line
|
// canon (docs/design/new-direct-messages-design/project/shared.jsx line
|
||||||
// 3-22). folds Icon wraps these in `<svg viewBox="0 0 24 24" fill="none">`,
|
// 3-22). folds Icon wraps these in `<svg viewBox="0 0 24 24" fill="none">`,
|
||||||
|
|
@ -513,6 +537,107 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Action-row buttons extracted into JSX consts so the bottom-slot
|
||||||
|
// markup stays readable. Single source for each button across whatever
|
||||||
|
// layout the composer evolves into; today the layout is a two-row
|
||||||
|
// strip — textarea on top, this trio below — on every platform.
|
||||||
|
const plusButton = (
|
||||||
|
<IconButton
|
||||||
|
onClick={() => pickFile('*')}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
fill="None"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
>
|
||||||
|
<Icon src={StreamComposerIcons.Plus} />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
const emojiButton = (
|
||||||
|
<UseStateProvider initial={undefined}>
|
||||||
|
{(emojiBoardTab: EmojiBoardTab | undefined, setEmojiBoardTab) => (
|
||||||
|
<PopOut
|
||||||
|
offset={16}
|
||||||
|
alignOffset={-44}
|
||||||
|
position="Top"
|
||||||
|
align="End"
|
||||||
|
anchor={
|
||||||
|
emojiBoardTab === undefined
|
||||||
|
? undefined
|
||||||
|
: emojiBtnRef.current?.getBoundingClientRect() ?? undefined
|
||||||
|
}
|
||||||
|
content={
|
||||||
|
<EmojiBoard
|
||||||
|
tab={emojiBoardTab ?? EmojiBoardTab.Emoji}
|
||||||
|
onTabChange={setEmojiBoardTab}
|
||||||
|
imagePackRooms={imagePackRooms}
|
||||||
|
returnFocusOnDeactivate={false}
|
||||||
|
onEmojiSelect={handleEmoticonSelect}
|
||||||
|
onCustomEmojiSelect={handleEmoticonSelect}
|
||||||
|
onStickerSelect={handleStickerSelect}
|
||||||
|
requestClose={() => {
|
||||||
|
setEmojiBoardTab((tab) => {
|
||||||
|
if (tab) {
|
||||||
|
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return tab;
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
ref={emojiBtnRef}
|
||||||
|
aria-pressed={!!emojiBoardTab}
|
||||||
|
onClick={() => setEmojiBoardTab(EmojiBoardTab.Emoji)}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
fill="None"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
>
|
||||||
|
<Icon src={StreamComposerIcons.Smile} />
|
||||||
|
</IconButton>
|
||||||
|
</PopOut>
|
||||||
|
)}
|
||||||
|
</UseStateProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const sendButton = (
|
||||||
|
<IconButton
|
||||||
|
onClick={(evt: ReactMouseEvent<HTMLButtonElement>) => {
|
||||||
|
submit();
|
||||||
|
// Defense-in-depth against the Android WebView's synthetic
|
||||||
|
// :hover/:focus-visible persistence (the CSS gate in
|
||||||
|
// RoomView.css.ts handles the same class of bug, but
|
||||||
|
// explicitly blurring the tap target also clears any
|
||||||
|
// lingering :focus state regardless of input-mode
|
||||||
|
// attribution).
|
||||||
|
evt.currentTarget.blur();
|
||||||
|
}}
|
||||||
|
variant="SurfaceVariant"
|
||||||
|
fill="None"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
>
|
||||||
|
<Icon src={StreamComposerIcons.Send} />
|
||||||
|
</IconButton>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Hour-of-day picks the placeholder variant. Memoised against
|
||||||
|
// (roomId, threadId) so the placeholder stays stable while the user is
|
||||||
|
// in one chat/thread and only re-rolls when they navigate elsewhere —
|
||||||
|
// and even then it's stable within the same wall-clock hour, so the
|
||||||
|
// change is most likely to surface to the user when they cross an
|
||||||
|
// hour boundary AND switch rooms (or reload the app). That cadence
|
||||||
|
// keeps the joke surprising without flipping mid-session.
|
||||||
|
const placeholderKey = useMemo(() => {
|
||||||
|
const idx = new Date().getHours() % COMPOSER_PLACEHOLDER_KEYS.length;
|
||||||
|
return COMPOSER_PLACEHOLDER_KEYS[idx];
|
||||||
|
// roomId and threadId are intentional re-roll triggers; the
|
||||||
|
// exhaustive-deps lint is happy with them too.
|
||||||
|
}, [roomId, threadId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ref}>
|
<div ref={ref}>
|
||||||
{selectedFiles.length > 0 && (
|
{selectedFiles.length > 0 && (
|
||||||
|
|
@ -606,7 +731,7 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
<CustomEditor
|
<CustomEditor
|
||||||
editableName="RoomInput"
|
editableName="RoomInput"
|
||||||
editor={editor}
|
editor={editor}
|
||||||
placeholder={t('Room.send_message')}
|
placeholder={t(placeholderKey)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
onPaste={handlePaste}
|
onPaste={handlePaste}
|
||||||
|
|
@ -653,83 +778,12 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
|
||||||
<Box
|
<Box
|
||||||
alignItems="Center"
|
alignItems="Center"
|
||||||
gap="200"
|
gap="200"
|
||||||
style={{ padding: `${toRem(4)} ${toRem(8)} ${toRem(6)}` }}
|
style={{ padding: `${toRem(2)} ${toRem(8)} ${toRem(4)}` }}
|
||||||
>
|
>
|
||||||
<IconButton
|
{plusButton}
|
||||||
onClick={() => pickFile('*')}
|
|
||||||
variant="SurfaceVariant"
|
|
||||||
fill="None"
|
|
||||||
size="300"
|
|
||||||
radii="300"
|
|
||||||
>
|
|
||||||
<Icon src={StreamComposerIcons.Plus} />
|
|
||||||
</IconButton>
|
|
||||||
<Box grow="Yes" />
|
<Box grow="Yes" />
|
||||||
<UseStateProvider initial={undefined}>
|
{emojiButton}
|
||||||
{(emojiBoardTab: EmojiBoardTab | undefined, setEmojiBoardTab) => (
|
{sendButton}
|
||||||
<PopOut
|
|
||||||
offset={16}
|
|
||||||
alignOffset={-44}
|
|
||||||
position="Top"
|
|
||||||
align="End"
|
|
||||||
anchor={
|
|
||||||
emojiBoardTab === undefined
|
|
||||||
? undefined
|
|
||||||
: emojiBtnRef.current?.getBoundingClientRect() ?? undefined
|
|
||||||
}
|
|
||||||
content={
|
|
||||||
<EmojiBoard
|
|
||||||
tab={emojiBoardTab ?? EmojiBoardTab.Emoji}
|
|
||||||
onTabChange={setEmojiBoardTab}
|
|
||||||
imagePackRooms={imagePackRooms}
|
|
||||||
returnFocusOnDeactivate={false}
|
|
||||||
onEmojiSelect={handleEmoticonSelect}
|
|
||||||
onCustomEmojiSelect={handleEmoticonSelect}
|
|
||||||
onStickerSelect={handleStickerSelect}
|
|
||||||
requestClose={() => {
|
|
||||||
setEmojiBoardTab((tab) => {
|
|
||||||
if (tab) {
|
|
||||||
if (!mobileOrTablet()) ReactEditor.focus(editor);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return tab;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
ref={emojiBtnRef}
|
|
||||||
aria-pressed={!!emojiBoardTab}
|
|
||||||
onClick={() => setEmojiBoardTab(EmojiBoardTab.Emoji)}
|
|
||||||
variant="SurfaceVariant"
|
|
||||||
fill="None"
|
|
||||||
size="300"
|
|
||||||
radii="300"
|
|
||||||
>
|
|
||||||
<Icon src={StreamComposerIcons.Smile} />
|
|
||||||
</IconButton>
|
|
||||||
</PopOut>
|
|
||||||
)}
|
|
||||||
</UseStateProvider>
|
|
||||||
<IconButton
|
|
||||||
onClick={(evt) => {
|
|
||||||
submit();
|
|
||||||
// Defense-in-depth against the Android WebView's synthetic
|
|
||||||
// :hover/:focus-visible persistence (the CSS gate in
|
|
||||||
// RoomView.css.ts handles the same class of bug, but
|
|
||||||
// explicitly blurring the tap target also clears any
|
|
||||||
// lingering :focus state regardless of input-mode
|
|
||||||
// attribution).
|
|
||||||
evt.currentTarget.blur();
|
|
||||||
}}
|
|
||||||
variant="SurfaceVariant"
|
|
||||||
fill="None"
|
|
||||||
size="300"
|
|
||||||
radii="300"
|
|
||||||
>
|
|
||||||
<Icon src={StreamComposerIcons.Send} />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -8,16 +8,20 @@ import {
|
||||||
import { VOJO_HORSESHOE_RADIUS_PX } from '../../styles/horseshoe';
|
import { VOJO_HORSESHOE_RADIUS_PX } from '../../styles/horseshoe';
|
||||||
|
|
||||||
// Main chat composer — Dawn canon (stream-v2-dawn.jsx line 285-307): a
|
// Main chat composer — Dawn canon (stream-v2-dawn.jsx line 285-307): a
|
||||||
// floating dark card with all-corners rounded geometry and a two-row layout
|
// floating dark card with all-corners rounded geometry. Two-row layout
|
||||||
// (input on top, action buttons below). Radius matches the silhouette
|
// (textarea on top, action strip with `+` / emoji / send below) — same
|
||||||
// horseshoe at the top of the chat (32px) for visual harmony — top and
|
// shape on every platform. Radius matches the silhouette horseshoe at the
|
||||||
// bottom of the chat surface mirror the same curvature. The default Editor
|
// top of the chat (32px) for visual harmony — top and bottom of the chat
|
||||||
// styling (pill R400, lighter SurfaceVariant fill) is preserved for the
|
// surface mirror the same curvature. The thread-drawer composer in
|
||||||
// thread-drawer composer, message-edit overlay and preview cases — none of
|
// `ThreadDrawer.tsx` also wraps `RoomInput` with this class
|
||||||
// them mount inside `ChatComposer`. The touch-hover gate at the bottom of
|
// (`${ThreadComposer} ${ChatComposer}`), so it inherits both the dark
|
||||||
// this file also covers `RoomTombstone` / `RoomInputPlaceholder` since they
|
// card chrome and the compact two-row geometry. The message-edit overlay
|
||||||
// share the wrap, which is intentional: their action buttons benefit from
|
// and `Editor.preview.tsx` mount `CustomEditor` directly without the
|
||||||
// the same Android-WebView stuck-:hover suppression.
|
// `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({});
|
export const ChatComposer = style({});
|
||||||
|
|
||||||
// Outer absolute-positioned wrapper for the composer overlay. Carries the
|
// Outer absolute-positioned wrapper for the composer overlay. Carries the
|
||||||
|
|
@ -55,29 +59,44 @@ globalStyle(`${ChatComposer} .${Editor}`, {
|
||||||
// clear of the side curves.
|
// clear of the side curves.
|
||||||
// * Vertical 6px — minimum that still leaves comfortable clearance
|
// * Vertical 6px — minimum that still leaves comfortable clearance
|
||||||
// from the top/bottom curves while keeping the card compact.
|
// from the top/bottom curves while keeping the card compact.
|
||||||
// Bottom-left geometry check (radius=32, centre at (32, card_bottom-32)):
|
// Bottom-left geometry check against the 32px corner curve (centre at
|
||||||
// * button-bottom = card_bottom - (6 outer + 6 row) = card_bottom - 12
|
// (32, card_bottom-32)) with action-row `padding: 2 8 4` (RoomInput.tsx):
|
||||||
// → curve x at y=12 from card bottom is ~7px from card left
|
// * button-bottom y-from-card-bottom = 6 (outer) + 4 (row pad-bot) = 10
|
||||||
// * button-left = 16 (outer) + 8 (row paddingLeft) = 24
|
// * curve-x at y=10 = 32 − √(32² − 22²) ≈ 8.76px
|
||||||
// → ~17px clearance between visible button edge and the curve
|
// * 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
|
// 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
|
// at 28px → ~25px clearance) is even more generous, which fits the
|
||||||
// single-line composer that grows downward when wrapping.
|
// 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)}`,
|
padding: `${toRem(6)} ${toRem(16)}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Visual alignment goal: typed-text glyph-start and Plus-icon glyph-start
|
// 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
|
// sit on the same vertical column at 28px from the card edge (mirrored on
|
||||||
// the right for Send).
|
// the right for Send). One column line through "Написать сообщение…" and
|
||||||
|
// the leading Plus.
|
||||||
// text-glyph-start = outer (16) + textarea paddingLeft (12) = 28
|
// text-glyph-start = outer (16) + textarea paddingLeft (12) = 28
|
||||||
// icon-glyph-left = outer (16) + row paddingLeft (8) + button pad (4) = 28
|
// icon-glyph-left = outer (16) + row paddingLeft (8) + button pad (4) = 28
|
||||||
// One column line through "Написать сообщение…" and the leading Plus.
|
|
||||||
globalStyle(`${ChatComposer} ${EditorTextareaScroll}:first-child ${EditorTextarea}`, {
|
globalStyle(`${ChatComposer} ${EditorTextareaScroll}:first-child ${EditorTextarea}`, {
|
||||||
paddingLeft: toRem(12),
|
paddingLeft: toRem(12),
|
||||||
});
|
});
|
||||||
globalStyle(`${ChatComposer} ${EditorTextareaScroll}:last-child ${EditorTextarea}`, {
|
globalStyle(`${ChatComposer} ${EditorTextareaScroll}:last-child ${EditorTextarea}`, {
|
||||||
paddingRight: toRem(12),
|
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
|
// NB: do NOT override `EditorPlaceholderTextVisual.paddingLeft` here. Slate
|
||||||
// renders the placeholder span absolutely-positioned (`position: absolute;
|
// renders the placeholder span absolutely-positioned (`position: absolute;
|
||||||
// top: 0`) inside the leaf text node — its origin is the contenteditable
|
// top: 0`) inside the leaf text node — its origin is the contenteditable
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue