style(search): Dawn panel for the switcher and grouped hairline rows for in-room results
This commit is contained in:
parent
7ea273eca8
commit
5843d75d89
4 changed files with 181 additions and 90 deletions
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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.06–0.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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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.06–0.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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue