style(search): Dawn panel for the switcher and grouped hairline rows for in-room results

This commit is contained in:
heaven 2026-06-04 01:00:23 +03:00
parent 7ea273eca8
commit 5843d75d89
4 changed files with 181 additions and 90 deletions

View file

@ -40,6 +40,7 @@ import {
import { DebounceOptions, useDebounce } from '../../hooks/useDebounce';
import { VirtualTile } from '../../components/virtualizer';
import { stopPropagation } from '../../utils/keyboard';
import { SectionLabel } from '../settings/styles.css';
type OrderButtonProps = {
order?: string;
@ -278,7 +279,7 @@ function SelectRoomButton({ roomList, selectedRooms, onChange }: SelectRoomButto
<MenuItem
data-room-id={roomId}
onClick={handleRoomClick}
variant={selected ? 'Success' : 'Surface'}
variant={selected ? 'Primary' : 'Surface'}
size="300"
radii="300"
aria-pressed={selected}
@ -364,24 +365,26 @@ export function SearchFilters({
const mx = useMatrixClient();
return (
<Box direction="Column" gap="100">
<Text size="L400">{t('Search.filter')}</Text>
<Box direction="Column" gap="200">
<Text as="span" className={SectionLabel}>
{t('Search.filter')}
</Text>
<Box gap="200" wrap="Wrap">
<Chip
variant={!global ? 'Success' : 'Surface'}
variant={!global ? 'Primary' : 'SurfaceVariant'}
aria-pressed={!global}
before={!global && <Icon size="100" src={Icons.Check} />}
outlined
radii="Pill"
onClick={() => onGlobalChange()}
>
<Text size="T200">{defaultRoomsFilterName}</Text>
</Chip>
{allowGlobal && (
<Chip
variant={global ? 'Success' : 'Surface'}
variant={global ? 'Primary' : 'SurfaceVariant'}
aria-pressed={global}
before={global && <Icon size="100" src={Icons.Check} />}
outlined
radii="Pill"
onClick={() => onGlobalChange(true)}
>
<Text size="T200">{t('Search.global')}</Text>
@ -400,7 +403,7 @@ export function SearchFilters({
return (
<Chip
key={roomId}
variant="Success"
variant="Primary"
onClick={() => onSelectedRoomsChange(selectedRooms.filter((rId) => rId !== roomId))}
radii="Pill"
before={

View file

@ -1,7 +1,10 @@
import React, { FormEventHandler, RefObject } from 'react';
import { Box, Text, Input, Icon, Icons, Spinner, Chip, config } from 'folds';
import { Box, Text, Icon, Icons, Spinner, Chip, color, config, toRem } from 'folds';
import { useTranslation } from 'react-i18next';
// Dawn hairline divider (the canon's rgba(255,255,255,0.060.08) row line).
const HAIRLINE = '1px solid rgba(255, 255, 255, 0.08)';
type SearchProps = {
active?: boolean;
loading?: boolean;
@ -24,46 +27,61 @@ export function SearchInput({ active, loading, searchInputRef, onSearch, onReset
};
return (
<Box as="form" direction="Column" gap="100" onSubmit={handleSearchSubmit}>
<span data-spacing-node />
<Text size="L400">{t('Search.search')}</Text>
<Input
ref={searchInputRef}
style={{ paddingRight: config.space.S300 }}
name="searchInput"
autoFocus
size="500"
variant="Background"
placeholder={t('Search.search_for_keyword')}
autoComplete="off"
before={
active && loading ? (
<Box as="form" direction="Column" onSubmit={handleSearchSubmit}>
{/* Integrated hairline search row shares the panel material instead of
a boxed folds Input under a floating «Поиск» label. */}
<Box
alignItems="Center"
gap="200"
style={{
paddingTop: config.space.S200,
paddingBottom: config.space.S200,
borderBottom: HAIRLINE,
}}
>
{active && loading ? (
<Spinner variant="Secondary" size="200" />
) : (
<Icon size="200" src={Icons.Search} />
)
}
after={
active ? (
)}
<input
ref={searchInputRef}
name="searchInput"
type="text"
// eslint-disable-next-line jsx-a11y/no-autofocus
autoFocus
placeholder={t('Search.search_for_keyword')}
autoComplete="off"
style={{
flex: 1,
appearance: 'none',
border: 'none',
outline: 'none',
background: 'transparent',
font: 'inherit',
fontSize: toRem(15),
color: color.Surface.OnContainer,
minWidth: 0,
}}
/>
{active ? (
<Chip
key="resetButton"
type="reset"
variant="Secondary"
variant="SurfaceVariant"
size="400"
radii="Pill"
outlined
after={<Icon size="50" src={Icons.Cross} />}
onClick={onReset}
>
<Text size="B300">{t('Search.clear')}</Text>
</Chip>
) : (
<Chip type="submit" variant="Primary" size="400" radii="Pill" outlined>
<Chip type="submit" variant="Primary" size="400" radii="Pill">
<Text size="B300">{t('Search.enter')}</Text>
</Chip>
)
}
/>
)}
</Box>
</Box>
);
}

View file

@ -35,7 +35,6 @@ import * as customHtmlCss from '../../styles/CustomHtml.css';
import { RoomAvatar, RoomIcon } from '../../components/room-avatar';
import { getMemberAvatarMxc, getMemberDisplayName, getRoomAvatarUrl } from '../../utils/room';
import { ResultItem } from './useMessageSearch';
import { SequenceCard } from '../../components/sequence-card';
import { UserAvatar } from '../../components/user-avatar';
import { useMentionClickHandler } from '../../hooks/useMentionClickHandler';
import { useSpoilerClickHandler } from '../../hooks/useSpoilerClickHandler';
@ -53,6 +52,9 @@ import {
import { useRoomCreators } from '../../hooks/useRoomCreators';
import { useRoomCreatorsTag } from '../../hooks/useRoomCreatorsTag';
// Dawn hairline for the grouped result-list panel + its internal row dividers.
const HAIRLINE = '1px solid rgba(255, 255, 255, 0.08)';
type SearchResultGroupProps = {
room: Room;
highlights: string[];
@ -215,8 +217,17 @@ export function SearchResultGroup({
</Text>
</Box>
</Header>
<Box direction="Column" gap="100">
{items.map((item) => {
{/* Grouped hairline-divided list one panel, internal row dividers,
instead of a pile of floating SequenceCard cards. */}
<Box
direction="Column"
style={{
border: HAIRLINE,
borderRadius: config.radii.R400,
overflow: 'hidden',
}}
>
{items.map((item, itemIndex) => {
const { event } = item;
const displayName =
@ -247,11 +258,13 @@ export function SearchResultGroup({
const usernameColor = legacyUsernameColor ? colorMXID(event.sender) : tagColor;
return (
<SequenceCard
<Box
key={event.event_id}
style={{ padding: config.space.S400 }}
variant="SurfaceVariant"
direction="Column"
style={{
padding: config.space.S400,
borderBottom: itemIndex < items.length - 1 ? HAIRLINE : undefined,
}}
>
<ModernLayout
before={
@ -288,14 +301,14 @@ export function SearchResultGroup({
</Username>
{tagIconSrc && <PowerIcon size="100" iconSrc={tagIconSrc} />}
</Box>
<Time ts={event.origin_server_ts} />
<Time ts={event.origin_server_ts} style={{ fontFamily: 'var(--font-mono)' }} />
</Box>
<Box shrink="No" gap="200" alignItems="Center">
<Chip
data-event-id={mainEventId}
onClick={handleOpenClick}
variant="Secondary"
radii="400"
variant="SurfaceVariant"
radii="Pill"
>
<Text size="T200">{t('Search.open')}</Text>
</Chip>
@ -314,7 +327,7 @@ export function SearchResultGroup({
)}
{renderMatrixEvent(event.type, false, event, displayName, getContent)}
</ModernLayout>
</SequenceCard>
</Box>
);
})}
</Box>

View file

@ -2,11 +2,10 @@ import FocusTrap from 'focus-trap-react';
import {
Avatar,
Box,
color,
config,
Icon,
Icons,
Input,
Line,
MenuItem,
Modal,
Overlay,
@ -16,7 +15,7 @@ import {
toRem,
} from 'folds';
import React, { useCallback } from 'react';
import { Trans, useTranslation } from 'react-i18next';
import { useTranslation } from 'react-i18next';
import { isKeyHotkey } from 'is-hotkey';
import { useAtom } from 'jotai';
import { useMatrixClient } from '../../hooks/useMatrixClient';
@ -35,10 +34,11 @@ import { UnreadBadge, UnreadBadgeCenter } from '../../components/unread-badge';
import { searchModalAtom } from '../../state/searchModal';
import { useKeyDown } from '../../hooks/useKeyDown';
import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
import { KeySymbol } from '../../utils/key-symbol';
import { isMacOS } from '../../utils/user-agent';
import { getDmUserId, useRoomSearch } from './useRoomSearch';
// Dawn hairline divider (the canon's rgba(255,255,255,0.060.08) row line).
const HAIRLINE = '1px solid rgba(255, 255, 255, 0.08)';
type SearchProps = {
requestClose: () => void;
};
@ -91,22 +91,45 @@ export function Search({ requestClose }: SearchProps) {
},
}}
>
<Modal size="400" style={{ maxHeight: toRem(400), borderRadius: config.radii.R500 }}>
<Modal
size="400"
style={{
maxHeight: toRem(400),
borderRadius: config.radii.R400,
backgroundColor: '#181a20',
border: HAIRLINE,
boxShadow: '0 16px 48px rgba(0, 0, 0, 0.5)',
}}
>
{/* ── Input row: hairline-divided, integrated (not a boxed Input) ── */}
<Box
shrink="No"
style={{ padding: config.space.S400, paddingBottom: 0 }}
direction="Column"
alignItems="Center"
gap="200"
style={{
padding: `${toRem(12)} ${config.space.S400}`,
borderBottom: HAIRLINE,
}}
>
<Input
<Icon size="200" src={Icons.Search} />
<input
ref={inputRef}
size="500"
variant="Background"
radii="400"
outlined
type="text"
placeholder={t('Search.search')}
before={<Icon size="200" src={Icons.Search} />}
autoComplete="off"
onChange={handleInputChange}
onKeyDown={handleInputKeyDown}
style={{
flex: 1,
appearance: 'none',
border: 'none',
outline: 'none',
background: 'transparent',
font: 'inherit',
fontSize: toRem(15),
color: color.Background.OnContainer,
minWidth: 0,
}}
/>
</Box>
<Box grow="Yes">
@ -131,7 +154,7 @@ export function Search({ requestClose }: SearchProps) {
)}
{roomsToRender.length > 0 && (
<Scroll ref={scrollRef} size="300" hideTrack>
<div style={{ padding: config.space.S400, paddingRight: config.space.S200 }}>
<div style={{ padding: config.space.S200, paddingRight: config.space.S100 }}>
{roomsToRender.map((roomId, index) => {
const room = getRoom(roomId);
if (!room) return null;
@ -153,6 +176,7 @@ export function Search({ requestClose }: SearchProps) {
exactParents && guessPerfectParent(mx, roomId, Array.from(exactParents));
const unread = roomToUnread.get(roomId);
const focused = listFocus.index === index;
return (
<MenuItem
@ -162,19 +186,35 @@ export function Search({ requestClose }: SearchProps) {
data-room-id={roomId}
data-space={room.isSpaceRoom()}
onClick={handleRoomClick}
variant={listFocus.index === index ? 'Primary' : 'Surface'}
aria-pressed={listFocus.index === index}
radii="400"
variant="Surface"
aria-pressed={focused}
radii="300"
style={{
// Fleet highlight: a violet tint + a 2px left bar
// instead of a solid Primary fill (Dawn selection).
backgroundColor: focused ? 'rgba(149, 128, 255, 0.08)' : undefined,
boxShadow: focused ? `inset 2px 0 0 ${color.Primary.Main}` : undefined,
}}
after={
<Box gap="100">
{dmUserServer && (
<Text size="T200" priority="300" truncate>
<b>{dmUserServer}</b>
<Text
size="T200"
priority="300"
truncate
style={{ fontFamily: 'var(--font-mono)' }}
>
{dmUserServer}
</Text>
)}
{!dm && perfectOrphanParent && (
<Text size="T200" priority="300" truncate>
<b>{getRoom(perfectOrphanParent)?.name ?? perfectOrphanParent}</b>
<Text
size="T200"
priority="300"
truncate
style={{ fontFamily: 'var(--font-mono)' }}
>
{getRoom(perfectOrphanParent)?.name ?? perfectOrphanParent}
</Text>
)}
{unread && (
@ -221,7 +261,13 @@ export function Search({ requestClose }: SearchProps) {
: room.name}
</Text>
{dmUsername && (
<Text as="span" size="T200" priority="300" truncate>
<Text
as="span"
size="T200"
priority="300"
truncate
style={{ fontFamily: 'var(--font-mono)' }}
>
@
{queryHighlightRegex
? highlightText(queryHighlightRegex, [dmUsername])
@ -241,14 +287,25 @@ export function Search({ requestClose }: SearchProps) {
</Scroll>
)}
</Box>
<Line size="300" />
<Box shrink="No" justifyContent="Center" style={{ padding: config.space.S200 }}>
<Text size="T200" priority="300">
<Trans
i18nKey="Search.help_text"
values={{ hotkey: `${isMacOS() ? KeySymbol.Command : 'Ctrl'} + k` }}
components={{ bold: <b /> }}
/>
{/* ── Mono keyboard-hint footer (hairline-divided) ── */}
<Box
shrink="No"
justifyContent="Center"
gap="300"
style={{
padding: `${toRem(8)} ${config.space.S300}`,
borderTop: HAIRLINE,
fontFamily: 'var(--font-mono)',
}}
>
<Text size="T200" priority="300" style={{ fontFamily: 'var(--font-mono)' }}>
{`↑↓ ${t('Search.kbd_select')}`}
</Text>
<Text size="T200" priority="300" style={{ fontFamily: 'var(--font-mono)' }}>
{`${t('Search.kbd_open')}`}
</Text>
<Text size="T200" priority="300" style={{ fontFamily: 'var(--font-mono)' }}>
{`esc ${t('Search.kbd_close')}`}
</Text>
</Box>
</Modal>