style(autocomplete): Dawn popover with mono handles, a fleet row highlight and a violet self-mention chip
This commit is contained in:
parent
baf23f9a45
commit
4c6f662939
7 changed files with 78 additions and 35 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { style } from '@vanilla-extract/css';
|
||||
import { DefaultReset, config } from 'folds';
|
||||
import { DefaultReset, color, config, toRem } from 'folds';
|
||||
|
||||
export const AutocompleteMenuBase = style([
|
||||
DefaultReset,
|
||||
|
|
@ -19,17 +19,51 @@ export const AutocompleteMenuContainer = style([
|
|||
},
|
||||
]);
|
||||
|
||||
// Dawn popover shell — a single hairline-bordered panel that floats above the
|
||||
// composer, mirroring the cmdK result-list look (panel #181a20 + faint fleet ring).
|
||||
export const AutocompleteMenu = style([
|
||||
DefaultReset,
|
||||
{
|
||||
maxHeight: '30vh',
|
||||
maxHeight: '40vh',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: color.SurfaceVariant.Container,
|
||||
border: `${toRem(1)} solid rgba(255, 255, 255, 0.07)`,
|
||||
borderRadius: config.radii.R400,
|
||||
boxShadow: '0 30px 80px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(149, 128, 255, 0.15)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
]);
|
||||
|
||||
// UPPERCASE, letter-spaced, muted mono section label — replaces the folds Header.
|
||||
export const AutocompleteMenuHeader = style([
|
||||
DefaultReset,
|
||||
{ padding: `0 ${config.space.S300}`, flexShrink: 0 },
|
||||
{
|
||||
flexShrink: 0,
|
||||
display: 'block',
|
||||
padding: `${config.space.S200} ${config.space.S300} ${config.space.S100}`,
|
||||
fontFamily: 'var(--font-mono)',
|
||||
fontSize: toRem(10),
|
||||
lineHeight: toRem(14),
|
||||
fontWeight: config.fontWeight.W500,
|
||||
letterSpacing: '0.12em',
|
||||
textTransform: 'uppercase',
|
||||
color: color.SurfaceVariant.OnContainer,
|
||||
opacity: 0.5,
|
||||
},
|
||||
]);
|
||||
|
||||
// JetBrains Mono technical handle (@user:server, #alias:server, !id, /command sig)
|
||||
// rendered at a muted tone inside an autocomplete row.
|
||||
export const AutocompleteMono = style({
|
||||
fontFamily: 'var(--font-mono)',
|
||||
opacity: 0.6,
|
||||
});
|
||||
|
||||
// Fleet highlight for the first (Tab-target) row: faint violet wash + a 2px
|
||||
// fleet left-border. Purely visual — reflects the onTabPress 'first item' logic.
|
||||
export const AutocompleteActiveRow = style({
|
||||
backgroundColor: 'rgba(149, 128, 255, 0.08)',
|
||||
boxShadow: `inset ${toRem(2)} 0 0 0 ${color.Primary.Main}`,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { ReactNode } from 'react';
|
||||
import FocusTrap from 'focus-trap-react';
|
||||
import { isKeyHotkey } from 'is-hotkey';
|
||||
import { Header, Menu, Scroll, config } from 'folds';
|
||||
import { Menu, Scroll, config } from 'folds';
|
||||
|
||||
import * as css from './AutocompleteMenu.css';
|
||||
import { preventScrollWithArrowKey, stopPropagation } from '../../../utils/keyboard';
|
||||
|
|
@ -38,9 +38,7 @@ export function AutocompleteMenu({ headerContent, requestClose, children }: Auto
|
|||
}}
|
||||
>
|
||||
<Menu className={css.AutocompleteMenu}>
|
||||
<Header className={css.AutocompleteMenuHeader} size="400">
|
||||
{headerContent}
|
||||
</Header>
|
||||
<span className={css.AutocompleteMenuHeader}>{headerContent}</span>
|
||||
<Scroll style={{ flexGrow: 1 }} onKeyDown={preventScrollWithArrowKey}>
|
||||
<div style={{ padding: config.space.S200 }}>{children}</div>
|
||||
</Scroll>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import React, { KeyboardEvent as ReactKeyboardEvent, useEffect, useMemo } from '
|
|||
import { Editor } from 'slate';
|
||||
import { Box, MenuItem, Text, toRem } from 'folds';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { AutocompleteQuery } from './autocompleteQuery';
|
||||
import { AutocompleteMenu } from './AutocompleteMenu';
|
||||
import * as css from './AutocompleteMenu.css';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { UseAsyncSearchOptions, useAsyncSearch } from '../../../hooks/useAsyncSearch';
|
||||
import { onTabPress } from '../../../utils/keyboard';
|
||||
|
|
@ -42,6 +44,7 @@ export function EmoticonAutocomplete({
|
|||
query,
|
||||
requestClose,
|
||||
}: EmoticonAutocompleteProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
|
||||
|
|
@ -85,8 +88,8 @@ export function EmoticonAutocomplete({
|
|||
});
|
||||
|
||||
return autoCompleteEmoticon.length === 0 ? null : (
|
||||
<AutocompleteMenu headerContent={<Text size="L400">Emojis</Text>} requestClose={requestClose}>
|
||||
{autoCompleteEmoticon.map((emoticon) => {
|
||||
<AutocompleteMenu headerContent={t('Room.autocomplete_emojis')} requestClose={requestClose}>
|
||||
{autoCompleteEmoticon.map((emoticon, index) => {
|
||||
const isCustomEmoji = 'url' in emoticon;
|
||||
const key = isCustomEmoji ? emoticon.url : emoticon.unicode;
|
||||
const customEmojiUrl = mxcUrlToHttp(mx, key, useAuthentication);
|
||||
|
|
@ -94,6 +97,7 @@ export function EmoticonAutocomplete({
|
|||
return (
|
||||
<MenuItem
|
||||
key={emoticon.shortcode + key}
|
||||
className={index === 0 ? css.AutocompleteActiveRow : undefined}
|
||||
as="button"
|
||||
radii="300"
|
||||
onKeyDown={(evt: ReactKeyboardEvent<HTMLButtonElement>) =>
|
||||
|
|
@ -121,7 +125,7 @@ export function EmoticonAutocomplete({
|
|||
)
|
||||
}
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} size="B400" truncate>
|
||||
<Text className={css.AutocompleteMono} style={{ flexGrow: 1 }} size="B400" truncate>
|
||||
:{emoticon.shortcode}:
|
||||
</Text>
|
||||
</MenuItem>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import { Editor } from 'slate';
|
|||
import { Avatar, Icon, Icons, MenuItem, Text } from 'folds';
|
||||
import { JoinRule, MatrixClient } from 'matrix-js-sdk';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { createMentionElement, moveCursor, replaceWithElement } from '../utils';
|
||||
import { getDirectRoomAvatarUrl } from '../../../utils/room';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import { AutocompleteQuery } from './autocompleteQuery';
|
||||
import { AutocompleteMenu } from './AutocompleteMenu';
|
||||
import * as css from './AutocompleteMenu.css';
|
||||
import { getMxIdServer, isRoomAlias } from '../../../utils/matrix';
|
||||
import { UseAsyncSearchOptions, useAsyncSearch } from '../../../hooks/useAsyncSearch';
|
||||
import { onTabPress } from '../../../utils/keyboard';
|
||||
|
|
@ -76,6 +78,7 @@ export function RoomMentionAutocomplete({
|
|||
query,
|
||||
requestClose,
|
||||
}: RoomMentionAutocompleteProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const mDirects = useAtomValue(mDirectAtom);
|
||||
|
||||
|
|
@ -86,12 +89,12 @@ export function RoomMentionAutocomplete({
|
|||
useCallback(
|
||||
(rId) => {
|
||||
const r = mx.getRoom(rId);
|
||||
if (!r) return 'Unknown Room';
|
||||
if (!r) return t('Room.autocomplete_unknown_room');
|
||||
const alias = r.getCanonicalAlias();
|
||||
if (alias) return [r.name, alias];
|
||||
return r.name;
|
||||
},
|
||||
[mx]
|
||||
[mx, t]
|
||||
),
|
||||
SEARCH_OPTIONS
|
||||
);
|
||||
|
|
@ -133,11 +136,11 @@ export function RoomMentionAutocomplete({
|
|||
});
|
||||
|
||||
return (
|
||||
<AutocompleteMenu headerContent={<Text size="L400">Rooms</Text>} requestClose={requestClose}>
|
||||
<AutocompleteMenu headerContent={t('Room.autocomplete_rooms')} requestClose={requestClose}>
|
||||
{autoCompleteRoomIds.length === 0 ? (
|
||||
<UnknownRoomMentionItem query={query} handleAutocomplete={handleAutocomplete} />
|
||||
) : (
|
||||
autoCompleteRoomIds.map((rId) => {
|
||||
autoCompleteRoomIds.map((rId, index) => {
|
||||
const room = mx.getRoom(rId);
|
||||
if (!room) return null;
|
||||
const dm = mDirects.has(room.roomId);
|
||||
|
|
@ -147,6 +150,7 @@ export function RoomMentionAutocomplete({
|
|||
return (
|
||||
<MenuItem
|
||||
key={rId}
|
||||
className={index === 0 ? css.AutocompleteActiveRow : undefined}
|
||||
as="button"
|
||||
radii="300"
|
||||
onKeyDown={(evt: ReactKeyboardEvent<HTMLButtonElement>) =>
|
||||
|
|
@ -154,7 +158,7 @@ export function RoomMentionAutocomplete({
|
|||
}
|
||||
onClick={handleSelect}
|
||||
after={
|
||||
<Text size="T200" priority="300" truncate>
|
||||
<Text className={css.AutocompleteMono} size="T200" priority="300" truncate>
|
||||
{room.getCanonicalAlias() ?? ''}
|
||||
</Text>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import React, { useEffect, KeyboardEvent as ReactKeyboardEvent } from 'react';
|
|||
import { Editor } from 'slate';
|
||||
import { Avatar, Icon, Icons, MenuItem, Text } from 'folds';
|
||||
import { MatrixClient, Room, RoomMember } from 'matrix-js-sdk';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { AutocompleteQuery } from './autocompleteQuery';
|
||||
import { AutocompleteMenu } from './AutocompleteMenu';
|
||||
import * as css from './AutocompleteMenu.css';
|
||||
import { useRoomMembers } from '../../../hooks/useRoomMembers';
|
||||
import { useMatrixClient } from '../../../hooks/useMatrixClient';
|
||||
import {
|
||||
|
|
@ -90,6 +92,7 @@ export function UserMentionAutocomplete({
|
|||
query,
|
||||
requestClose,
|
||||
}: UserMentionAutocompleteProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const useAuthentication = useMediaAuthentication();
|
||||
const { roomId } = room;
|
||||
|
|
@ -137,7 +140,7 @@ export function UserMentionAutocomplete({
|
|||
getMemberDisplayName(room, member.userId) ?? getMxIdLocalPart(member.userId) ?? member.userId;
|
||||
|
||||
return (
|
||||
<AutocompleteMenu headerContent={<Text size="L400">Mentions</Text>} requestClose={requestClose}>
|
||||
<AutocompleteMenu headerContent={t('Room.autocomplete_users')} requestClose={requestClose}>
|
||||
{query.text === 'room' && (
|
||||
<UnknownMentionItem
|
||||
userId={roomAliasOrId}
|
||||
|
|
@ -152,7 +155,7 @@ export function UserMentionAutocomplete({
|
|||
handleAutocomplete={handleAutocomplete}
|
||||
/>
|
||||
) : (
|
||||
autoCompleteMembers.map((roomMember) => {
|
||||
autoCompleteMembers.map((roomMember, index) => {
|
||||
const avatarMxcUrl = roomMember.getMxcAvatarUrl();
|
||||
const avatarUrl = avatarMxcUrl
|
||||
? mx.mxcUrlToHttp(avatarMxcUrl, 32, 32, 'crop', undefined, false, useAuthentication)
|
||||
|
|
@ -160,6 +163,9 @@ export function UserMentionAutocomplete({
|
|||
return (
|
||||
<MenuItem
|
||||
key={roomMember.userId}
|
||||
className={
|
||||
index === 0 && query.text !== 'room' ? css.AutocompleteActiveRow : undefined
|
||||
}
|
||||
as="button"
|
||||
radii="300"
|
||||
onKeyDown={(evt: ReactKeyboardEvent<HTMLButtonElement>) =>
|
||||
|
|
@ -167,7 +173,7 @@ export function UserMentionAutocomplete({
|
|||
}
|
||||
onClick={() => handleAutocomplete(roomMember.userId, getName(roomMember))}
|
||||
after={
|
||||
<Text size="T200" priority="300" truncate>
|
||||
<Text className={css.AutocompleteMono} size="T200" priority="300" truncate>
|
||||
{roomMember.userId}
|
||||
</Text>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { KeyboardEvent as ReactKeyboardEvent, useCallback, useEffect, use
|
|||
import { Editor } from 'slate';
|
||||
import { Box, config, MenuItem, Text } from 'folds';
|
||||
import { Room } from 'matrix-js-sdk';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Command, useCommands } from '../../hooks/useCommands';
|
||||
import {
|
||||
AutocompleteMenu,
|
||||
|
|
@ -10,6 +11,7 @@ import {
|
|||
moveCursor,
|
||||
replaceWithElement,
|
||||
} from '../../components/editor';
|
||||
import * as css from '../../components/editor/autocomplete/AutocompleteMenu.css';
|
||||
import { UseAsyncSearchOptions, useAsyncSearch } from '../../hooks/useAsyncSearch';
|
||||
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||
import { useKeyDown } from '../../hooks/useKeyDown';
|
||||
|
|
@ -36,6 +38,7 @@ export function CommandAutocomplete({
|
|||
query,
|
||||
requestClose,
|
||||
}: CommandAutocompleteProps) {
|
||||
const { t } = useTranslation();
|
||||
const mx = useMatrixClient();
|
||||
const commands = useCommands(mx, room);
|
||||
const commandNames = useMemo(() => Object.keys(commands) as Command[], [commands]);
|
||||
|
|
@ -71,17 +74,11 @@ export function CommandAutocomplete({
|
|||
});
|
||||
|
||||
return autoCompleteNames.length === 0 ? null : (
|
||||
<AutocompleteMenu
|
||||
headerContent={
|
||||
<Box grow="Yes" direction="Row" gap="200" justifyContent="SpaceBetween">
|
||||
<Text size="L400">Commands</Text>
|
||||
</Box>
|
||||
}
|
||||
requestClose={requestClose}
|
||||
>
|
||||
{autoCompleteNames.map((commandName) => (
|
||||
<AutocompleteMenu headerContent={t('Room.autocomplete_commands')} requestClose={requestClose}>
|
||||
{autoCompleteNames.map((commandName, index) => (
|
||||
<MenuItem
|
||||
key={commandName}
|
||||
className={index === 0 ? css.AutocompleteActiveRow : undefined}
|
||||
as="button"
|
||||
radii="300"
|
||||
style={{ height: 'unset' }}
|
||||
|
|
@ -97,7 +94,7 @@ export function CommandAutocomplete({
|
|||
gap="100"
|
||||
justifyContent="SpaceBetween"
|
||||
>
|
||||
<Text style={{ flexGrow: 1 }} size="B400" truncate>
|
||||
<Text className={css.AutocompleteMono} style={{ flexGrow: 1 }} size="B400" truncate>
|
||||
{`/${commandName}`}
|
||||
</Text>
|
||||
<Text truncate priority="300" size="T200">
|
||||
|
|
|
|||
|
|
@ -147,9 +147,9 @@ export const Mention = recipe({
|
|||
base: [
|
||||
DefaultReset,
|
||||
{
|
||||
backgroundColor: color.SurfaceVariant.Container,
|
||||
color: color.SurfaceVariant.OnContainer,
|
||||
boxShadow: `0 0 0 ${config.borderWidth.B300} ${color.SurfaceVariant.ContainerLine}`,
|
||||
// Flat Dawn pill — violet-tinted fill + lavender text, no boxShadow ring.
|
||||
backgroundColor: 'rgba(149, 128, 255, 0.12)',
|
||||
color: color.Primary.MainHover,
|
||||
padding: `0 ${toRem(2)}`,
|
||||
borderRadius: config.radii.R300,
|
||||
fontWeight: config.fontWeight.W500,
|
||||
|
|
@ -158,14 +158,14 @@ export const Mention = recipe({
|
|||
variants: {
|
||||
highlight: {
|
||||
true: {
|
||||
backgroundColor: color.Success.Container,
|
||||
color: color.Success.OnContainer,
|
||||
boxShadow: `0 0 0 ${config.borderWidth.B300} ${color.Success.ContainerLine}`,
|
||||
// Self-mention reads as the fleet brand accent (not green status).
|
||||
backgroundColor: color.Primary.Container,
|
||||
color: color.Primary.OnContainer,
|
||||
},
|
||||
},
|
||||
focus: {
|
||||
true: {
|
||||
boxShadow: `0 0 0 ${config.borderWidth.B300} ${color.SurfaceVariant.OnContainer}`,
|
||||
boxShadow: `0 0 0 ${config.borderWidth.B300} ${color.Primary.Main}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue